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

minecraft-asset-loader

v1.5.4

Published

Fetch anything from any Minecraft: Java Edition version, in Node and the browser, touching the network as little as possible

Readme

minecraft-asset-loader

Fetch anything from any Minecraft: Java Edition version ever released, in Node.js and the browser. List versions, download assets, search files, and export whole versions.

npm version jsDelivr License: MPL 2.0

Features

  • Every version Mojang has published, from the earliest alphas to the latest snapshot
  • Bedrock Edition support, served from the official bedrock-samples releases
  • An asset index mode, serving the Java asset indexes as standalone versions
  • Textures, models, blockstates, item definitions, sounds, structures, and languages
  • Both resource pack and data pack assets included
  • Search and filter files, with the best matches sorted first
  • A built-in cache: download a version once, and later lookups are near instant
  • Game code is never downloaded, so fetches are quick and exports stay small

Install

For Node.js, or the browser through a bundler:

npm install minecraft-asset-loader

Or in the browser, import it straight from a CDN:

import MinecraftAssets from "https://cdn.jsdelivr.net/npm/minecraft-asset-loader/+esm"

Quick Start

import MinecraftAssets from "minecraft-asset-loader"

const assets = new MinecraftAssets()

const png = await assets.getTexture("block/stone")            // png bytes
const model = await assets.getModel("block/stone")            // parsed json
const files = await assets.list("assets/minecraft/textures")  // every texture the version has
const hits = await assets.search("stone", { extension: "png" })

version is a version id, or one of three keywords, and defaults to "release":

new MinecraftAssets({ version: "26.1.2" })    // an exact version id
new MinecraftAssets({ version: "release" })   // the latest release, the default
new MinecraftAssets({ version: "snapshot" })  // the newest snapshot, even when a newer release exists
new MinecraftAssets({ version: "newest" })    // release or snapshot, whichever is newer

Every method also takes { version } to request a specific version for that one call:

const old = await assets.getTexture("blocks/stone", { version: "1.8.9" })

Nothing is fetched until a call needs it. Listing a version costs a small partial request, and the first file read downloads the useful part of the version's jar: range requests skip game code and other unwanted files, which are around 70% of the total size. After that, every read, search, and export of that version is served from memory and the cache.

Documentation

new MinecraftAssets(options)

All options are optional:

| Option | Default | Description | |---|---|---| | type | "java" | What to serve: "java", "assets", or "bedrock". See Asset index mode and Bedrock edition | | version | "release" | The default version: an id, "release", "snapshot", or "newest" | | objects | false | Include asset objects in listings and reads by default | | cacheDir | OS temp folder | Where the built-in cache lives (Node.js) | | cacheSize | 1 GB | Cap on the built-in cache in bytes, least recently used purged first. Infinity never purges | | cacheKey | | Namespaces the built-in cache, so separate instances can keep separate caches | | cacheAPI | | Use your own cache in place of the built-in cache | | proxy | | A URL prefix, or a function given the URL and returning the one to request. See Browser | | manifest | | The listing to use instead of fetching it: Mojang's version manifest, or GitHub's releases array for bedrock. See Your own manifest | | manifestExpiry | response headers | How long in milliseconds to trust a fetched manifest. Infinity keeps one copy for the whole session | | onManifestProgress | | Called with a 0 to 1 ratio while the manifest is built. Only assets, which resolves every version's asset index and takes hundreds of requests | | onManifestUpdate | | Serves an expired manifest from the cache and refetches it in the background, calling this with assets.manifest once a changed one is in place. Browser only. Without it an expired manifest is awaited | | minecraft | auto-detected | Serve from the local .minecraft installation when it has the file, before downloading. A string sets the folder, false disables. See Local .minecraft |

Versions

assets.manifest reads Mojang's version manifest. It is fetched on first use, cached, and trusted until its manifestExpiry, so quick restarts do not refetch it. The methods below all live on assets.manifest:

| Method | Description | |---|---| | .versions(filter?) | Every version, newest first. See Filters | | .version(id) | One version by id or keyword. Returns null when the version is unknown | | .latest() | { release, snapshot, newest } | | .details(id) | The raw per-version JSON. This includes the jar and asset index URLs, JVM arguments, required libraries, and more | | .update(manifest?) | Replace the in-memory manifest, or call with nothing to trigger an early refresh |

const { release, snapshot } = await assets.manifest.latest()
const version = await assets.manifest.version("26.1.2")

assets.version can be updated at any time. Use .setVersion(id) to replace the version used by default for future calls. It returns the version entry:

await assets.setVersion("snapshot")
assets.version   // "26.3-snapshot-10"
assets.channel   // "snapshot"

Both .version and .channel read from the in-memory manifest, so they start null until a call has fetched it.

Filters

versions() takes a type keyword, a function, or an array of either. Arrays are combined, without duplicates. VersionType holds the keywords:

import MinecraftAssets, { VersionType } from "minecraft-asset-loader"

await assets.manifest.versions(VersionType.RELEASE)                            // "release"
await assets.manifest.versions([VersionType.SNAPSHOT, VersionType.BETA])       // "snapshot" and "old_beta"
await assets.manifest.versions(all => all.filter(v => v.id.startsWith("26."))) // 2026 releases

| Keyword | Matches | Description | |---|---|---| | RELEASE | release | Full releases | | SNAPSHOT | snapshot | Snapshots, pre-releases, and release candidates | | BETA | old_beta | The beta era, b1.0 to b1.8.1 | | ALPHA | old_alpha | The alpha era and everything before it, back to rd-132211 | | MAIN | custom | The newest patch of each minor line (the Nether Update as 1.16.5, the Copper Age as 1.21.10). Useful for a version picker of "unique" game versions | | MODERN | custom | Versions with the modern assets/ layout, 13w24a and later. See Version entries |

Your own keywords can be added through standard assignments:

VersionType.RC = all => all.filter(v => v.id.includes("-rc"))
await assets.manifest.versions(VersionType.RC)

Version entries

A version entry contains the data for that version from the manifest, alongside version-scoped versions of the asset methods.

const v = await assets.manifest.version("1.8.9")
await v.getTexture("blocks/stone")
await assets.getTexture("blocks/stone", { version: "1.8.9" })

Supported methods are details, list, search, file, read, every getter, loadObjects, and export.

legacyLayout is a custom property set on versions. It is true for versions before 13w24a, where the jar had no assets/ folder, and all assets sat within the root. The getters (except .getLang()) do not work on versions with the legacy asset layout, so use .read() instead. LEGACY_ASSETS_BEFORE is exported as the timestamp of that cutoff.

Your own manifest

If you already fetch the manifest yourself, you can pass it in as manifest to the constructor. Use assets.manifest.update(json) to update it later with a newer version. When a manifest was provided by you, it will not expire and relies on you to keep it refreshed. .update() with no argument refetches from Mojang and hands control back to the library.

What to hand over is whatever that type is built from, not the list it ends up with:

| Type | Pass | |---|---| | java | Mojang's version manifest | | assets | Mojang's version manifest, the same document | | bedrock | The array GitHub's releases endpoint returns |

assets lists asset indexes rather than versions, and works them out by reading each version's details, so the manifest you provide is the input to that. The index list itself stays the library's to build and cache, and it will read those details, from the cache when they are there and over the network when they are not.

Files

| Method | Description | |---|---| | .list(folder?, options?) | Every file in the version, in a flat list, sorted by path. With a folder, only the files beneath it | | .list(folder?, { folders: true }) | { files, folders }, the direct contents of one folder: the files as file entries, the subfolders as folder entries | | .search(query, options?) | Files matching a query. See Search | | .file(path, options?) | One file entry, or null | | .read(path, options?) | The bytes of one file as a Uint8Array, or null |

All of them take { version, objects }. read also takes prefer, see Asset objects.

await assets.list()                                               // every file, flat
await assets.list("assets/minecraft/textures")                    // every file beneath a folder, flat
await assets.list({ folders: true })                              // browse the root: { files, folders }
await assets.list("assets/minecraft", { folders: true })          // browse a folder
await assets.search("stone", { extension: "png" })                // best matches first
await assets.file("assets/minecraft/textures/block/stone.png")    // one entry, nothing downloaded yet
await assets.read("data/minecraft/loot_table/blocks/stone.json")  // one file's bytes

File entries

Each file is { path, source, size, crc, hash }. The source is where it came from: "jar" or "object". crc is only there on jar files, and hash only on asset objects. source only exists on the "java" type, since bedrock and assets files all come from one place. File entries also get read() and raw() methods:

const stone = await assets.file("assets/minecraft/textures/block/stone.png")
stone.size          // 157
await stone.read()  // the bytes
await stone.raw()   // the bytes as stored: { compression: "deflate-raw" | null, bytes }

raw() skips decompression, for handing files to a worker or another zip cheaply. readZip entries have it too.

Folder entries

folder.list() can be used to list files from this folder instead of from the root. Same formatting as the main list method.

Folder entries are { path, source, objects }, where path is the full path from the root, source is "jar", "object", or "both", covering every file beneath it ("java" type only, like on files), and objects is the setting the folder was listed with.

Folders have the list, search, file, and read methods for getting files directly from the folder. These automatically use the folder's objects setting unless it is manually overridden.

const { folders } = await assets.list("assets/minecraft", { folders: true })
const textures = folders.find(f => f.path.endsWith("/textures"))

await textures.search("stone", { extension: "png" })   // scoped beneath textures/
await textures.read("block/stone.png")
await textures.list()                                  // every file beneath textures/
await textures.list("block")                           // every file beneath textures/block/
await textures.list("block", { folders: true })        // browse textures/block/

Asset objects

Sounds, languages, the panoramas, and a few other files are not stored in the jar. They are served individually from a separate host, by hash, listed in each version's asset index. They are big (around 480 MB combined for a modern version), and cost an extra fetch to list, so they are opt in. Pass objects: true to the constructor, or to any call:

const everything = await assets.list({ objects: true })
const harp = await assets.file("assets/minecraft/sounds/note/harp.ogg", { objects: true })

With objects on, the listing merges the jar and the objects into one view, each file appearing once. getSound, getLang, and loadObjects never need the flag, they use objects automatically.

Some files exist in both the jar and the objects: the title screen panoramas for example. The object copy is used by default. Pass prefer: "jar" to read to take the jar copy instead.

loadObjects bulk downloads asset objects. It returns a Map with file paths as the keys and the downloaded bytes as the values. A failed download is missing from the map rather than thrown.

| Option | Default | Description | |---|---|---| | filter | | A function given each path, or an array of paths and/or file entries | | concurrency | 32 | How many downloads can run at once | | onProgress | | Called with (done, total) as each file finishes | | cache | true | Add the downloaded objects to the cache | | version | | Same as everywhere else |

const notes = await assets.loadObjects({ filter: p => p.includes("/sounds/note/"), onProgress: (done, total) => {} })

Search

await assets.search("stone", { extension: "png", limit: 10 })
await assets.search("", { root: "assets/minecraft/models/block" })   // everything under a folder
await assets.search(/panorama_\d/)                                   // a RegExp works too

Searching is managed by path-search-sort

| Option | Default | Description | |---|---|---| | root | | Only paths starting with this prefix, from the top of the tree: "assets/minecraft/models" means inside that exact folder | | path | | Only paths passing through these folders, wherever they sit: "block" means under any block/ folder, however deep | | extension | | Only these file types: one extension or several, leading dot optional | | filter | | Your own predicate over the path, only called for what already matched | | limit | | Cap on results, applied after sorting | | folders | true | Also match folder names: files inside a matching folder rank after name matches | | caseSensitive | false | Case matters, except for the extension filter; RegExp queries ignore it | | spaces | "_" | What a typed space becomes, e.g. "_" for underscore-named files; RegExp queries ignore it | | version, objects | | Same as everywhere else |

Results come back sorted into a sensible order, best matches first. See how it sorts for the details.

A namespace prefix in the query is stripped, so minecraft:stone and stone search the same.

Getters

Getters are shortcuts for the most commonly wanted file types, using identifiers instead of paths. Paths do still work if provided.

<ns> in the table below is the id's namespace: minecraft by default, or the namespace option when given. A prefix on the id overrides both: getTexture("namespace:id") reads from assets/namespace/textures/id.

| Method | Reads from | Returns | |---|---|---| | .getTexture(id, options?) | assets/<ns>/textures/ | The png bytes. With meta: true, { data, meta } where meta is the parsed .mcmeta or null | | .getModel(id, options?) | assets/<ns>/models/ | Parsed json | | .getBlockstate(id, options?) | assets/<ns>/blockstates/ | Parsed json | | .getItemDefinition(id, options?) | assets/<ns>/items/ | Parsed json | | .getSound(id, options?) | assets/<ns>/sounds/ | The ogg bytes | | .getStructure(id, options?) | data/<ns>/structure/ | The structure nbt bytes. Supports older pack layouts too | | .getLang(code, options?) | assets/<ns>/lang/ | A key to string object, older .lang files included |

Identifiers can be written however you have them: the namespace, the kind folder, and the extension are each optional, and a full path works too:

await assets.getTexture("block/stone")
await assets.getTexture("block/stone.png")
await assets.getModel("minecraft:block/stone")
await assets.getBlockstate("blockstates/stone")
await assets.getSound("minecraft:sounds/note/pling")
await assets.getStructure("igloo/top.nbt")
await assets.getTexture("assets/minecraft/textures/block/stone.png")

Export

Use the export function to export all the files to a zip or a folder. Fetches anything that is not already cached. When dir is not provided it returns a zip file.

| Option | Default | Description | |---|---|---| | filter | | A function given each path, or an array of paths and/or file entries | | dir | | Node.js: write a folder tree here instead of returning a zip. Returns the number of files written | | objects | | Include the asset object files too | | concurrency | 32 | How many fetches can run at once | | onProgress | | Called with (done, total) as each file finishes | | version | | Same as everywhere else |

const zip = await assets.export({ filter: p => p.includes("note_block") })
const count = await assets.export({ dir: "./out", version: "b1.7.3" })

Local .minecraft

In Node.js, the "java" and "assets" types check the local .minecraft installation before downloading anything: version jars, asset indexes, and asset objects are all served straight from disk when the launcher already has them. This is on by default at the platform's standard location, minecraft: "path/to/.minecraft" points it somewhere else, and minecraft: false turns it off.

Caching

Everything Mojang serves except the version manifest is immutable and named by hash, so it is cached forever and never revalidated. The manifest is cached with its expiry. Once it has expired, a browser serves the cached copy straight away and refreshes it in the background, while Node.js waits for the fresh copy.

| Method | Description | |---|---| | .loadJar(options?) | Downloads a version's jar data immediately instead of waiting for the first read. onProgress is called with (done, total) in bytes, for a loading bar | | .cacheStats() | The cache as { files, size } in bytes | | .listCache() | Every cached file as { key, size }, biggest first | | .clearCache(key?) | Clears the full cache, or just one file when passed its key | | .setCacheSize(bytes) | Changes the cache limit and evicts down to it. null disables eviction |

await assets.loadJar({ version: "26.1.2", onProgress: (done, total) => {} })

await assets.cacheStats()                 // { files: 212, size: 48213096 }
const [biggest] = await assets.listCache()
await assets.clearCache(biggest.key)

Your own cache

Pass cacheAPI to replace the built-in cache with anything that can store bytes:

const assets = new MinecraftAssets({
  cacheAPI: {
    read: key => store.get(key),       // Uint8Array, or undefined
    write: (key, bytes) => store.set(key, bytes),
    delete: key => store.delete(key),  // optional, powers clearCache(key)
    clear: () => store.clear(),        // optional, powers clearCache()
    list: () => Array.from(store, ([key, bytes]) => ({ key, size: bytes.length }))  // optional, powers cacheStats() and listCache()
  }
})

Keys are strings starting meta/ or blobs/, and values are always a Uint8Array. cacheStats() and listCache() are null when list() is not provided.

Browser

As of the time this was created, the version manifest, version details, and jar hosts all allow cross-origin requests. These can all be fetched directly without the need for a proxy. The asset objects host sends no CORS headers at all, so a CORS proxy server must be used to access them:

const assets = new MinecraftAssets({
  proxy: url => url.includes("resources.download.minecraft.net") ? "https://my-proxy/" + url : false
})

A string is used as a prefix on every request. A function can be used for more advanced URLs, or to proxy specific URLs only. Returning false requests the original URL directly, without the proxy.

Asset index mode

Pass type: "assets" to serve the Java asset indexes on their own. The version list becomes the asset index versions instead of the game versions, and content is just the files in the index, with no jar involved:

const assets = new MinecraftAssets({ type: "assets" })

await assets.manifest.versions()          // one entry per asset index: "34", "1.19", "legacy", "pre-1.6", ...
await assets.getSound("note/pling")
await assets.getLang("de_de")

Many game versions share one index, so the list is short (around 56 entries). Each entry keeps the index's own metadata: sha1, url, size, totalSize (the combined size of every file it points at), plus first and last, the game versions that introduced it and most recently used it. An index counts as "release" when any release uses it, and "snapshot" when only snapshots do. Building the list means sampling game version details to find where the indexes change, a few hundred small fetches, so the first call takes a moment; after that it is all cached.

Everything works from the index's own file list: list, search, read, export, and the getters. All entries are object-backed, so the objects flag is ignored. What resolves is whatever the index actually holds, which is sounds and non-English translations on modern indexes (everything else lives in the jar), plus icons, music, and .lang files on the older ones. getTexture, getModel, getBlockstate, getItemDefinition, and getStructure are usually null since that content never left the jar. loadJar prefetches every file in the index with byte progress.

In a browser this mode needs the proxy for content, since everything comes from the asset objects host.

Bedrock edition

Pass type: "bedrock" to serve Bedrock Edition instead, from Mojang's official bedrock-samples releases:

const assets = new MinecraftAssets({ type: "bedrock" })

await assets.getTexture("blocks/stone")
await assets.getModel("entity/allay")
await assets.getLang("en_US")

Versions come from the GitHub releases, and previews are the "snapshot" channel. The GitHub API is touched as little as possible to try and avoid the strict rate limits.

Each version is a single zip. It is downloaded whole on first use, cached, and everything is served from it. The first touch of a version costs the full download (roughly 150 MB) and everything after is instant. loadJar forces the download with byte progress, though total can be null when the server does not declare a length.

The getters map to Bedrock's own layout:

| Method | Reads from | |---|---| | .getTexture(id, options?) | resource_pack/textures/, .png with a .tga fallback. meta: true returns the .texture_set.json sidecar | | .getModel(id, options?) | resource_pack/models/, .geo.json with a .json fallback | | .getBlockstate(id, options?) | behavior_pack/blocks/ | | .getItemDefinition(id, options?) | behavior_pack/items/ | | .getSound(id, options?) | resource_pack/sounds/, .fsb with an .ogg fallback | | .getLang(code, options?) | resource_pack/texts/ | | .getStructure(id, options?) | Nothing. bedrock-samples ships no structure files, so this is always null |

Bedrock has no namespaces: a minecraft: prefix is accepted and stripped, and any other namespace is a miss. There are no asset objects in bedrock, so the objects flag is ignored and loadObjects givs you nothing.

In a browser, the zip hosts send no CORS headers, so bedrock mode needs the proxy for content. The version list needs no proxy.

Module exports

import MinecraftAssets, { VersionType, LEGACY_ASSETS_BEFORE, readZip, writeZip } from "minecraft-asset-loader"

| Export | Description | |---|---| | MinecraftAssets | The class. The default export | | VersionType | The filter keywords | | LEGACY_ASSETS_BEFORE | The 13w24a timestamp, marking which jars use the legacy layout | | readZip(bytes) | The library's zip reader. Parses any zip into { path, size, crc } entries with a read() method, decompressing nothing until an entry is read | | writeZip(files, options?) | The library's zip writer. Takes a Map or plain object of path to bytes, or an array of entries with read(), so a readZip result or the library's own file entries repack directly. Entries that also carry raw(), crc and size are copied as stored, so nothing is decompressed or recompressed |

const entries = readZip(bytes)                    // [{ path, size, crc }], each with read()
const icon = await entries.find(e => e.path === "pack.png").read()

const zip = await writeZip({ "pack.mcmeta": mcmetaBytes, "pack.png": iconBytes })
const textures = await writeZip(await assets.list("assets/minecraft/textures/block"))

writeZip options:

| Option | Default | Description | |---|---|---| | compress | true | false stores everything uncompressed, faster for already-compressed files. Copied entries keep their stored form either way | | concurrency | 32 | How many files can pack at once | | onProgress | | Called with (done, total) as each file finishes |

License

MPL-2.0 © Ewan Howell