npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ringozz/godot

v4.7.2-685

Published

Node-API bindings for Godot Engine — call Godot classes from JavaScript

Readme

@ringozz/godot

Node-API bindings for the Godot Engine — call Godot classes, value types, and utility functions from JavaScript.

This is the core runtime package. The React layer is a separate package (@ringozz/react-godot).

Installation

bun add @ringozz/godot

The package ships no engine binary itself — it auto-selects a platform addon from its optional dependencies on install: @ringozz/godot-macos-arm64, @ringozz/godot-windows-x86_64, or @ringozz/godot-web-wasm32 (the WASM build is installed everywhere and used on web). No project.godot is required — the engine boots from engine defaults with res:// = your working directory (if a project.godot is present at cwd, it is loaded and res:// still maps to cwd).

Usage

On desktop, the engine is already running when you import — getGodot() returns the instance synchronously. Pump frames with runGodot():

import { runGodot } from '@ringozz/godot';
import { Engine } from '@ringozz/godot/Engine';
import { SceneTree } from '@ringozz/godot/SceneTree';

const tree = Engine.getMainLoop() as SceneTree;
const root = tree.root;

const label = new Label();
label.text = 'hello from godot-node';
root.addChild(label);

const done = runGodot(); // pumps frames until aborted

Web

The wasm engine boots asynchronously, so on web the entry script must boot it before importing the app: @ringozz/godot/boot's preloadGodot() starts the engine, and @ringozz/godot/runtime then reads the booted module synchronously (the package's exports map routes ./boot to the web leaf in browser-targeted bundles). This keeps the shared module graph top-level-await free, which is also what makes Bun's HMR dev server work. On desktop preloadGodot() is a no-op — the engine boots at import.

// your web entry (referenced by <script type="module"> in the HTML)
import { preloadGodot } from '@ringozz/godot/boot';
await preloadGodot();
await import('./app.ts'); // may now import @ringozz/godot / react-godot freely

(That's exactly what dev/demo.html.ts does in this repo. A second <script type="module"> tag can't replace the dynamic import — Bun's fullstack HTML bundler merges all script entries into one chunk, so the browser's in-order script/TLA serialization never applies.)

What's exported where

| Specifier | Contents | |---|---| | @ringozz/godot | Value types (Vector3, Color, …), global enums, heap types, utility functions, global constants, runGodot(), RAF polyfills | | @ringozz/godot/ClassName | Classes, one per module — e.g. @ringozz/godot/Label, @ringozz/godot/Node | | @ringozz/godot/runtime | Low-level dispatch: _C, _S, _R, _G, _P, _V, toValueType, getGodot, GodotVar | | @ringozz/godot/boot | Platform boot: getNativeModule(); web preloadGodot() (awaited before importing the app; no-op on desktop) | | @ringozz/godot/load | Async resource loading: loadResourceAsync, loadAsset, loadAssetPack (native .pck packs), configurePacks/loadPackEntry (content packs) | | @ringozz/godot/bake | buildAssetPacks(opts, run?) + the godot pack subcommand: bake the project's boot .pck plus one per declared content pack, with their loose-texture documents. opts.cwd picks the project root and run replaces the editor, so a test can bake a temp project | | @ringozz/godot/pack | The pack protocol the bake writes and the plugin/runtime read: PACK_DOC/RELEASE/FILES_DIR and the boot's names, parseDoc/serializeDoc/parseRelease, packEntries/godotEntry/entryId/pckOf, and resolveEntry (what to mount and stage for one entry) | | @ringozz/godot/debug | initDebug(), dumpStr, snapshot, inspect, click, find, gc |

Class-scoped enums use bare names (import ProcessMode from @ringozz/godot/Node, not NodeProcessMode).

Value types

Godot's built-in value types (Vector3, Color, Transform3D, …) are named tuples — labeled array aliases, not classes. Import each type module as a namespace and call its math as functions; constructors are fromXxx factories:

import * as v3 from '@ringozz/godot/Vector3';
import * as basis from '@ringozz/godot/Basis';
import type { Vector3 } from '@ringozz/godot/Vector3';

const v: Vector3 = [1, 2, 3];            // flat tuple, labels in the type
const n = v3.normalized(v);              // math API per type module
const b = basis.fromAxisAngle([0, 1, 0], 1.57);
  • Shapes: flat types are scalar-labeled (Vector3 = [x: number, y: number, z: number], Color = [r, g, b, a?]); nested types are row-labeled tuples of the flat alias (Transform3D = [x, y, z, origin], AABB = [position, size]). Wrong-arity arrays fail typecheck; Color's a is the only optional element (alpha defaults 1).
  • Getters/setters/props all use the same tuple shapes — node.position = [1, 2, 3] reads back [x, y, z]; JSX props in @ringozz/react-godot accept them too.
  • A plain tuple stored in a Variant-typed slot (setMeta, a direct tweenProperty final value) becomes a generic Godot Array — a tuple carries no type. Wrap it with toValueType(node, 'prop', tuple) from @ringozz/godot/runtime to store the typed value.

Async resource loading

The engine loads pre-baked native resources (.scn, .ctex, …) through ResourceLoader. On desktop these are real files (res:// = cwd); on web there is no filesystem, so the asset's bundled files — .import sidecars as text and imported products (.scn/.ctex) as emitted asset paths — are staged into Emscripten's in-memory MEMFS (transient, no IndexedDB, no persistent copy) by the generated asset modules, handing the fetched bytes straight to MEMFS via Godot's own copyToFS (FS.createDataFile with canOwn — zero copy, JS-heap only). On web, compressed-texture products are decoded first: for .ctex (and the layered .ctexarray / .ccube / .ccubearray) the embedded PNG/WebP blobs go through the browser's createImageBitmap and the container is rebuilt as raw RGBA (DATA_FORMAT_IMAGE) — per layer for arrays — so no image codec runs in wasm. A built-in transcoder instead rebuilds the container as a device-native block-compressed texture via @ludicon/spark.js, choosing the format from the source file's name — normal maps → BC5/EAC-RG, alpha albedo, HDR/sky and .svg vector art → BC7/ASTC, terrain weight planes → BC4/EAC-R, everything else (colour/ORM) → BC1/ETC2 first, each with a BC7/ASTC and finally a raw-RGBA fallback — and encoding every layer of an array/cubemap in parallel; its chunk is fetched along with the rest of the app (so it isn't on the critical path to the first texture), the WebGPU device is warmed as soon as it lands, and any decode/encode failure falls back to the raw RGBA path. .ctex3d has its own layout and is left to the engine. The source file itself is never fetched.

import { loadResourceAsync } from '@ringozz/godot/load';
import { PackedScene } from '@ringozz/godot/PackedScene';
import { Texture2D } from '@ringozz/godot/Texture2D';

const scene = await loadResourceAsync('res://models/foo.gltf', PackedScene); // PackedScene
const node  = scene.instantiate();
const tex   = await loadResourceAsync('res://textures/icon.jpg', Texture2D); // Texture2D
  • loadResourceAsync(path, cls, files?) — generic over the Godot class: cls.name is the ResourceLoader type hint, and the return type is InstanceType<typeof cls>. Loads in the background (loadThreadedRequest + status polling). Godot default arguments apply: the napi dispatch truncates trailing undefined args, so loadThreadedRequest uses its default CACHE_MODE_REUSE and the resource stays cached in the engine (scene re-references don't re-read the file). On web it only works for paths already staged into MEMFS (generated asset modules handle this). When the asset's files map is passed, the staged product files (.ctex/.scn/native sources) are deleted from MEMFS after the resource loads — only the .import sidecars are kept, since they route imported source paths to their products. The decoded texture bytes don't linger in the JS heap. files lists overlap (a scene and the model it instantiates can share one loose texture), so a product may need re-staging after another module's cleanup: materialize creates the parent directory itself and treats an already-staged file as done, because the engine's copyToFS throws on a path that is already there while FileAccess.fileExists('res://…') cannot see the Emscripten spelling of it.
  • loadAsset(path, cls, files, data, ready) — the generated asset modules' entry point (also usable directly): returns { materialize, load }. It checks the engine's ResourceCache first (when the asset is already loaded, load resolves to the cached instance and staging is skipped), stages the bundled files via Godot's copyToFS (zero-copy handover of the fetched bytes; a no-op on desktop where the files already exist), loads via loadResourceAsync, and memoizes load on data (pass import.meta.hot.data) so HMR re-evaluations reuse the same fulfilled promise.

Asset packs

godot pack (the godot bin, @ringozz/godot/bake) bakes a project's assets for the runtime: it imports them, asks dump-deps.gd for every source's loose (WebP-backed) textures — including a plan, which names refs no resource edge exposes (it warns when a plan resolves no ref at all, or is stamped under another extras namespace — that reading has no static check) — and exports one native .pck — the engine-converted binary resources plus a remap table — through the project's committed export_presets.cfg. loadAssetPack(url) fetches a pack into MEMFS and hands it to ProjectSettings.loadResourcePack; packs are add-only, so several can be mounted and res:// resolves across all of them.

The bake produces one boot pack — the project's own sources, exported through its committed export_presets.cfg preset, and always mounted by the preload plugin, which is what makes a scene's own run-time paths (a script named by a constant, a level chosen at run time) resolvable — plus one pack per directory under dlc/ that declares itself with a package.json: that file gives the pack its name, its dependencies on other packs and its exports entries, the pack is that directory and nothing else (the bake rewrites the preset's one exclude_filter line per pack), and the bake fails if a pack references anything but the boot sources and its declared dependencies. The boot pack's document is .godot/packs/packs.json (needs = which loose files each source needs staged, stage = the descriptor and product of each loose file the pack owns); a content pack's is a pack.json emitted beside its .pck (PACK_DOC — not package.json, which is the pack's own manifest in its source directory, an input the bake reads) (its declared fields plus needs/stage — the same document the boot's packs.json is). Deploying those packs independently — versioning, an index, a CDN — is the host's business, not the bake's.

import { loadAssetPack } from '@ringozz/godot/load';
await loadAssetPack('/packs/shared.pck');   // usually the preload plugin does this per asset

Content packs

A content pack is deployed apart from the app, so nothing about it is in the bundle: the runtime reads a release (which version of which pack to mount — a bun.lock, or the plain map a local bake emits) and each mounted pack's document, then loads a declared entry. Point the runtime at the packs once, at start-up:

import { configurePacks } from '@ringozz/godot/load';
// The host owns the base: a pack's name, its version and one of its files, addressed its own way.
configurePacks({
	at: (name, version, file) => `https://cdn.example/packs/${name}@${version}/${file}`,
	release: 'https://cdn.example/packs/release.json',   // or the map itself: `{ '@tiv/beach': { version, deps } }`
});

The boot pack — the app's own sources, which ship beside it — needs no config: the bake renders it into a module (.godot/packs/boot-pack.js: its .pck, plus a bundled URL per loose product it owns), which every imported asset pulls in, and configurePacks merges, so the app's at and the boot's config may land in either order. A pack declares its entries (exports → the godot condition, the res:// path) and ships no entry module: the preload plugin serves one, built from the bake's documents, with one named export per entry. So importing a pack is an ordinary import — and the bake emits the types it resolves to (package.d.ts, named by the pack's types):

import { level } from '@tiv/dlc-beach';   // → a PackedScene, via loadPackEntry(pack, 'level', …)

Mounting is add-only, so every pack in the closure stays mounted for the page's lifetime.

Asset imports

Asset imports are resolved by @ringozz/godot/preload (a Bun plugin, registered via bunfig.toml preload for bun run/bun test and via [serve.static] plugins for Bun.serve's fullstack HTML routes; the module's default export is the single plugin object) into a module per asset. Only JS imports are virtualized — HTML asset references (<link rel="icon">, <img src>) and CSS url() refs fall through to Bun's normal (content-hashed) asset handling, so favicons and other web assets co-exist with Godot imports:

import foo from './models/foo.gltf';
const scene = await foo;          // PackedScene

Each asset module's default export is its load promise (const { materialize, load } = loadAsset(resPath, Class, files, import.meta.hot.data, mounting) — the default is load, memoized on the module's HMR data), started eagerly at module import, and it also exports a materialize promise (its files staged). The files Godot reads are bundled into the module — .import sidecars are inlined as text, imported products (.ctex) are emitted as files — from the bake's document, never from scanning the source text: dump-deps.gd walked the project at bake time and wrote, per source, the loose (WebP-backed) texture sources the wasm engine cannot decode, and the pack's stage gives each of those its sidecar and its product. A packed source resolves inside the mounted .pck (the engine-converted resources plus a remap table); only its loose textures are staged per file, transcoded in the browser. The mounting promise comes from the bake's boot pack — the module the bake generates beside the packs (.godot/packs/boot-pack.js): its .pck, plus a bundled URL for every loose product it owns — so importing any asset is also what mounts the app's own pack, and the runtime can stage a content pack's loose files even when the boot owns them. The modules are virtual (namespace godot), and every import of the same asset resolves to the same one (evaluated once) — so the promise is a stable singleton, read with await or React 19's use():

import foo from './models/foo.gltf';
import { Suspense, use } from 'react';

function Foo() {
  const scene = use(foo);   // suspends until the scene loads
  return <FooView scene={scene} />;
}

<Suspense fallback={<Label text="Loading…" />}>
  <Foo />
</Suspense>

Each extension maps to its Godot class (source assets only — gltf/tscn/obj → PackedScene, gd → GDScript, gdshader → Shader, gdshaderinc → ShaderInclude, jpg/jpeg/png/webp/svg → CompressedTexture2D, exr/hdr → TextureLayered, wav → AudioStreamWAV, ogg → AudioStreamOggVorbis, mp3 → AudioStreamMP3, ttf/otf → FontFile, tres → Resource, po → Translation), declared in src/assets.d.ts. Imported products (.scn/.ctex) are never imported directly. .gd scripts import as Promise<GDScript>, so import script from './x.gd'; const gd = await script; works like any other resource — and when a .tscn references the same .gd, Godot's ResourceLoader cache returns the same instance (parsed once).

Textures are shared only through Godot's ResourceLoader path cache. The glTF import pipeline may produce per-scene texture instances (same res:// path, separate objects), so don't assume two scenes referencing the same image share one texture object.

Debugging

import { initDebug } from '@ringozz/godot/debug';
initDebug();
// singletons + helpers now on globalThis.$: $.Engine, $.tree, $.dumpStr(node), $.snapshot([pattern]), $.inspect(path), $.click(path), $.find([pattern], [type]), $.paused, $.gc()
  • dumpStr(obj) renders value types inline ((1.0, 2.0, 3.0), arrays, dictionaries) via Godot's str() and dumps objects via Godot's var_to_str, with the node's name prepended.
  • snapshot(pattern?, node?) dumps the scene tree from the root Window as Class "name" (N children) lines; pass a string (case-insensitive substring) or RegExp to filter. $.snapshot() with no arg dumps the whole tree.
  • inspect(path) resolves a node by absolute NodePath (/root/Box) or name/glob pattern (findChild) and returns its dumpStr.
  • click(path) synthesizes a left-click at the center of a Control via Window.pushInput (device-space coordinates), returning the node's dumpStr or null if it isn't a Control.
  • find(pattern?, type?) returns a JS array of nodes matching a name glob and/or class ($.find('*', 'RigidBody3D') lists all physics bodies).
  • $.tree is the live SceneTree; $.paused = true freezes the scene tree for inspection; $.paused = false resumes.

initDebug also registers an uncaughtException handler that keeps the process alive, polyfills DOMRect (not defined in Bun, needed by getBoundingClientRect()), and adds getBoundingClientRect() to CanvasItem/Node3D (Node3D covers descendant meshes; returns CSS-pixel coordinates).

Notes

  • Memory: non-RefCounted objects (Node, Node2D, Node3D, …) must call free() explicitly. RefCounted objects are managed by Godot's ref counting; calling free() clears the JS wrapper (decrements the ref).
  • Class-registration tree-shaking: ClassDB.instantiate('X') requires the JS class to be value-imported (import { Label } from '@ringozz/godot/Label'). If a class is only type-used (e.g. as Label casts), bundlers may drop its registration side effect, and wrappers fall back to an ancestor class. Render through JSX is unaffected. Workaround: keep a value reference (void Label;).

Development

gen/ is generated (and gitignored) by dev/codegen.ts from godot --dump-extension-api-with-docs. From the repo root: bun run prebuild (codegen + typecheck), bun run predev (imports dev/assets/ via the editor), bun run build (native addon), bun run test, and bun run dev (web dev server). See AGENTS.md for the full workflow.

The package also ships a godot bin (src/editor.ts) used by the scripts: it resolves GODOT_PATH → a system godot on PATH → ~/.local/bin/godot-<version>, and downloads the pinned official editor from godotengine/godot-builds into ~/.local/bin/godot-<version> when none is found (the versioned filename makes a version bump re-provision).