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

@twin-digital/mc-dev-kit

v0.4.0

Published

Discovers the Minecraft Bedrock packs in a workspace and reports each one validated and completed.

Readme

@twin-digital/mc-dev-kit

The development kit for Minecraft Bedrock pack packages in a workspace. It has three halves:

  • discovery — discoverPacks() reports every pack in the workspace, its manifest completed and validated, without building anything
  • build — packBuild() returns a bundler config fragment that builds a package's packs into its output tree
  • archive — mc-pack-archive cuts a built output tree into the single .mcaddon a release uploads

A pack's source manifest.json is partial by design: it names no header.name, no header.version, no version for a dependency on a pack in the same workspace, and no modules[].entry. The kit completes each from the owning package's package.json, so raising a package's version and building is what produces a new version of its pack.

Install

npm install --save-dev @twin-digital/mc-dev-kit

ESM only, with type declarations. The package embeds no bundler at run time — tsdown is a development dependency of the kit and of the packages that build with it, never a runtime one.

The pack package layout

A pack package holds its script sources under src/, its source packs in fixed, kind-named directories, and its built packs under dist/:

packages/my-pack/
  package.json                          the name and version the manifests complete from
  src/main.ts                           the script bundle's entry
  behavior_pack/
    manifest.json                       partial: no name, no version, no entry
    functions/…, entities/…             copied verbatim
  resource_pack/
    manifest.json
    textures/…                          copied verbatim
  dist/
    behavior_pack/manifest.json         the completed manifest
    behavior_pack/scripts/main.js       the bundle
    resource_pack/…

Both packs are optional; a package holding neither is not a pack package. Nothing under a pack directory is a build input — a pack directory holds only content, and a scripts/ directory there fails the build.

Discovery

import { discoverPacks, resolveWorkspaceRoot } from '@twin-digital/mc-dev-kit'

const packs = await discoverPacks()
const broken = await discoverPacks({ filter: { status: 'invalid' } })

const root = await resolveWorkspaceRoot({ from: packageDir })

Every pack found comes back in one flat list, each entry valid or invalid. A valid entry carries its completed manifest, its uuid and version, and the locations of its source directory, output directory, and built script; an invalid one carries the problems that invalidated it. A fault after enumeration becomes a problem on an entry rather than a thrown error, so a single broken pack does not hide the rest.

Discovery reads the source tree only. It reports where a pack's output belongs and never creates, reads, or modifies anything there.

Build

The build half is an export a consuming package's bundler configuration takes up, not a command the package runs. In the opus monorepo it reaches the configuration through a tsdown.config.d/ fragment merged over the generated base:

// packages/my-pack/tsdown.config.d/packs.ts
import { packBuild } from '@twin-digital/mc-dev-kit/build'

export default packBuild({ packageDir: new URL('../..', import.meta.url).pathname })

packBuild takes one required option, packageDir — the filesystem path of the package the build is for — and returns a fragment carrying a single plugin that performs the whole build. Building the package then produces, for each of its packs:

  • the completed manifest at dist/<kind>_pack/manifest.json, as two-space JSON with a trailing newline — never a copy of the source manifest
  • the script bundle at dist/behavior_pack/scripts/main.js, one unminified ESM chunk, with the module_name dependencies the completed manifest declares left external and everything else inlined
  • every other pack file copied verbatim — dotfiles, .lang files, textures, unknown extensions and all — except the source manifest. With namespacing on, files carrying declared names are rewritten instead — see Namespacing

A finished build loads as it stands, with nothing further to do to it.

The package's dist/ becomes the build's to own. Output the build did not write is deleted at the end of it, so a file deleted or renamed in source is gone from the output after the next build with no clean step first. A package taking up the fragment therefore devotes its bundler configuration and its output tree to its packs.

Nothing is rewritten that did not change. Every file, the script bundle included, is compared with what already sits at its path and written only on a difference, so a consumer watching the output tree and keying on modification times sees only real changes. The build writes no report of what it changed — the output tree is the report.

A rebuild is triggered by any input the build reads. The pack source directories, the source manifests, the package's package.json, the package.json of each workspace package a pack depends on, and the vendored_pack/ tree of every dependency the build vendors are all registered as watch inputs, so changing a texture or bumping a version rebuilds even though no module graph reaches those files.

A package whose behavior pack declares no script module, or which holds no behavior pack at all, builds too: the fragment falls back to a virtual entry and the chunk nothing claims is pruned at the end of the build.

What fails the build

  • the package holds no pack, or sits under no workspace root
  • the kit reports one of the package's packs invalid — its problems are printed, and no sibling pack in the package is built
  • the kit's enumeration rejects, which any malformed package.json in the workspace can cause
  • a behavior pack declares a script module while src/main.ts is not there
  • a pack directory holds a scripts/ directory
  • an @minecraft/-scoped import the completed manifest does not declare resolves to nothing
  • the package vendors anything while no namespace is set, or vendors a kind it holds no pack of
  • with namespacing on: a source name already carrying a namespace, a bare entity name carrying a dot, content whose names the build cannot rewrite, an own bare asset reference a merged pack declares, a bare reference in any merged pack that its own supplier declares, a token-qualified reference its named dependency does not declare, a library field entry naming a package outside its dependencies, an own declaration sitting in a merged prefix's qualifier position, a reference only an un-admitted transitive supplier declares, two merged dependencies resolving to one entity prefix, a prefix outside its charset, and a bare entity name or prefix landing in the reserved mcdk_claim_ spelling

Namespacing

packBuild takes an optional namespace setting beside packageDir. Setting it turns namespacing on: true derives the namespace from the package's own name — the @ dropped and the / a hyphen, so @twin-digital/wizards becomes twin-digital-wizards — and a string names one directly. A namespace holds only lowercase letters, digits, underscore, hyphen and dot; anything else fails the build naming the character. Left unset, nothing is namespaced and names reach the output exactly as the source spells them.

export default packBuild({ packageDir: new URL('../..', import.meta.url).pathname, namespace: true })

A namespace you choose by hand is conventionally claimed at the Bedrock-OSS add-on registry, which refuses a namespace another entry already holds. The build neither reads the registry nor requires an entry in it — claiming is how you avoid colliding with other add-ons that register.

With namespacing on, names are written bare in pack content — wizard, geometry.wizard — and the build writes them into their namespaced spellings; a source name that already carries a prefix fails the build naming the file and the name. What each declared name becomes:

  • entity identifiers, and the entity.<id>.name / item.spawn_egg.entity.<id>.name localization keys derived from them, carry the namespace: your own wizard builds as <namespace>:wizard, and a vendored pack's minion as <namespace>:<prefix>.minion — the prefix being the per-dependency token you choose (see the vendor block below). A bare entity name may not contain a dot, which the build reserves as the prefix separator; a dotted declaration fails naming the file and the name
  • every other name the package itself declares — geometry, textures, materials, render controllers, animations, animation controllers — carries the namespace as a token written into the name's own structure: geometry.wizard builds as geometry.<namespace>.wizard, a texture at textures/entity/wizard.png moves to textures/<namespace>/entity/wizard.png, a material wizard becomes <namespace>_wizard, and render controllers, animations and animation controllers gain the token as a name segment
  • a vendored asset's names carry the vendored library's package token plus a 16-hex sha256 content hash instead — geometry.<library token>-<hash>.minion — so an identical name always means identical bytes: two packages vendoring one library version share names for unchanged assets (whichever definition wins, they are the same), and where content differs each package addresses exactly the bytes it built against. Upgrading a library changes the names of the assets whose content changed and only those, so a vendored asset never changes appearance underneath you, and unchanged assets deduplicate across consumers

Only names the packs declare are rewritten, along with the references to them, so the two pack halves still join. A reference resolves against the pack that wrote it first — a vendored pack's internal references stay internal, and your own references prefer your own declarations. Your own content also references a vendored entity by its composed spelling, <prefix>.<name>, which rewrites to <namespace>:<prefix>.<name>.

Your own content can direct an asset reference the same way: a merged dependency's prefix in the qualifier position binds the reference to that dependency's declaration, and the build rewrites it to the final hashed name exactly as it rewrites a bare reference — the prefix is a source-level directing token that never reaches the output. The qualifier position per name kind: geometry.<prefix>.<name>, animation.<prefix>.<name>, controller.render.<prefix>.<name>, controller.animation.<prefix>.<name>, textures/<prefix>/<path> for a texture, and <prefix>.<name> for a material reference or parent. A qualified reference whose dependency declares no such asset fails naming the file, the spelling, and the dependency; and an own declaration sitting in a qualifier position — a geometry named geometry.<prefix>.smoke, a texture under textures/<prefix>/ — fails the build naming the declaration and the prefix, so a qualified spelling can never be captured by your own names (change that dependency's prefix to resolve it).

In your own content, bare means yours or vanilla, prefixed means theirs: a bare asset reference binds to your own declaration or not at all — it never binds to a merged dependency. A bare reference that matches nothing of yours but that one or more merged packs declare fails the build naming every declarer and printing each one's qualified spelling as the fix, rather than silently binding — or silently shadowing a vanilla name of the same spelling. A reference to a name no reachable vendored pack declares — geometry.evoker.v1.8, a vanilla texture or material — is copied through as written; one that only an un-merged transitive supplier declares fails the build naming the referencing file, the name, the supplying package, and the fix. Vendored content resolves within its own world, never yours: bare is that pack's own or vanilla, and its suppliers are reached through its own minecraft.vendor tokens over its own direct dependencies — geometry.fx-core.orb, fx-core.spark. A bare name one of its own suppliers declares fails printing its token form (a supplier gaining a name turns a vanilla reference loud, never silently rebound); a token naming an un-admitted direct supplier gets the admission diagnosis; and a name nothing in its world declares copies as written — your own declarations and your other dependencies included, which are vanilla from the library's seat. Adding a dependency or declaring a name of your own never changes what a library's references mean.

Script sources are never rewritten: code spells a namespaced identifier through @twin-digital/mc-pack-runtime's packId helper, which reads what the build injects into the bundle ahead of all module code — the namespace, the pack token, and the sorted prefixes of every merged dependency. In modules belonging to a merged dependency, the build binds the runtime import itself: their packId is wrapped to prepend the dependency's prefix, so a library's compiled-in calls — computed names included — land in its own entity cell with nothing passed per call. Your own modules import the runtime unwrapped.

The build also stamps a type family, mcdk_pack_<package token>, on every entity type the namespaced pack declares, and adds one claim entity type, <namespace>:mcdk_claim_<package token>, to every namespaced pack with a behavior half. The runtime package's checked calls and foreignNamespaceClaims() read both; bare entity names starting with mcdk_claim_ are reserved and fail the build.

A namespaced pack may hold entity definitions (behavior and client), geometries, textures, materials, render controllers, animations, animation controllers, .lang files, scripts/, and its manifest. A .json, .material, .lang or .mcfunction file anywhere else may carry names the build cannot rewrite, so it fails the build naming the file; any other file — dotfiles, images outside textures/, unknown extensions — copies unchanged.

Vendoring shared packs

A pack several packages depend on can be built into each of them as content of its own. The shared package puts its content under vendored_pack/, holding a kind-named subdirectory per half and no manifest.json, so the package bears no pack of its own:

my-lib/
  package.json
  vendored_pack/
    behavior_pack/
      entities/minion.json
    resource_pack/
      entity/minion.json

Vendoring is configured by one shipped package.json field, serving consumers and libraries alike:

{
  "minecraft": {
    "vendor": {
      "@rpg-libs/spell-fx": { "prefix": "fx" },
      "@acme/particle-core": {}, // a transitive supplier, admitted explicitly
    },
  },
}

The field has one meaning in both roles: the token this package writes in its own source to reference that dependency's content. A token resolves in one order everywhere: your own explicit prefix entry, else the dependency's shipped minecraft.defaultAlias — a sibling of vendor a shared package may declare ({ "minecraft": { "defaultAlias": "fx" } }) — else the dependency's unscoped npm name (@rpg-libs/spell-fx is written as spell-fx.*). A token holds lowercase letters, digits, underscore and hyphen — never a dot, the separator — and two dependencies resolving to one token fail the build naming both, with an explicit prefix as the fix; an invalid defaultAlias fails its own package's build, and at a consumer's build fails naming the library with the same fix. A library changing its defaultAlias is a breaking change: your token references fail loudly, listing the resolved tokens — they never rebind or silently rename — and pinning the old token as an explicit prefix entry restores your build and preserves shipped-world entity ids exactly. For the package whose build ships the packs, the token additionally fixes the output entity-id segment, <namespace>:<prefix>.<name>; a vendored library's entries are source-side aliases only — the output naming of a supplier's entities is always governed by the shipping consumer's token for that supplier.

What merges is explicit: the vendored_pack/ of every package in the shipping package's own dependencies, plus any package named in its minecraft.vendor field — never devDependencies — workspace sibling and installed dependency alike. A field entry naming a package outside dependencies is, for the shipping consumer, how a transitive supplier is admitted; for a vendored library it is a fault — a library reaching deeper promotes the supplier to a direct dependency. A package reached along several dependency paths merges once, and the transitive tree is still walked read-only, so a reference that only an un-admitted supplier could satisfy fails with the fix spelled out rather than shipping broken.

A package holding a vendored_pack/ tree is validated by its own build as well: content kinds, its declarations, its tokens against its own direct dependencies (a token naming an uninstalled supplier is reported as such), and every reference. A package holding only a vendored tree validates and emits nothing. The consumer's build re-validates everything regardless — a shipped tree is untrusted input, and a workspace sibling may never have run its own build.

For an installed dependency to work, the shared package must publish its vendored_pack/ tree — add it to the files field of its package.json:

{
  "name": "@scope/my-lib",
  "files": ["vendored_pack"],
}

Vendoring requires a namespace, and the vendoring package must hold its own source manifest of every kind it vendors — the vendored content merges into that pack, under the vendoring package's namespace and header uuid, so each vendoring package ships one behavior pack and one resource pack whatever it vendors, and the same shared pack's entity identifiers get a different spelling and identity in every package that takes it up. The build reads the vendored source tree directly; the depended-on package never needs to have been built. Its vendored_pack/ tree joins the watch inputs, and the mc-pack-archive command archives the merged output tree as it stands, vendored content included. A vendored definition file lands beside your own with its library's token prefixed to its basename — models/<library token>.minion.geo.json — so two merged packs shipping one relative path never contend for it.

A vendored pack may hold entity definitions, geometries, textures, materials, render controllers, animations, animation controllers, and localization entries keyed by an entity identifier; content of any other kind fails the build naming the file. Names never contend across the merged packs: entity identifiers differ by the prefix segment, asset names by each pack's token. One asset name declared by two files of one vendored pack still fails naming both, since its two content hashes would leave references nothing to pick. A file more than one merged pack contributes entries to — texts/en_US.lang, say — is composed; one the build cannot compose fails it, naming both contributors.

The settings the fragment states

The fragment sets clean, format, target, platform, shims, dts, sourcemap, minify, noExternal, entry, outDir, outputOptions.entryFileNames, and inputOptions.resolve.conditionNames itself rather than inheriting them, so it behaves the same merged over a shared base as it does alone. Those are the keys a consuming package's own bundler configuration must not quietly supply instead.

target is es2022, platform is neutral, shims is false, and format is esm because that is what the Bedrock script engine accepts. clean is false because emptying the output directory first would take the end-of-build prune's inputs with it.

Archive

mc-pack-archive

The command takes no arguments and works on the package directory it is run in. It cuts that package's built output tree into one .mcaddon holding one .mcpack per pack, each member holding the contents of its pack's output directory at the member's root. The archive is named <package name with its npm scope stripped>-<version>.mcaddon and written into .release-assets/, which is created and cleared first so a previous version's archive is not published beside the current one.

It reads the output tree and consults nothing else — it never calls the kit and never runs a build, so a tree stale with respect to source is archived as it stands. A package with no output tree fails the command, naming the directory that was not there.