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

@studiokloek/vite-assets-plugin

v0.6.1

Published

Vite plugin and CLI that packs sprites, sounds and fonts from tagged asset directories.

Readme

@studiokloek/vite-assets-plugin

Packs the sprites, sounds and fonts of a project from tagged directories: a Vite plugin that runs before every vite serve or build, and a kloek-assets CLI for everything else. It replaces @studiokloek/pixi-texturepacker, @studiokloek/sounds-packer and the AssetPack pipes of @studiokloek/vite-react-config.

  • Sprites: every {spr} directory becomes one PixiJS spritesheet (json + png/jpg), in every configured resolution (@2x), with trimming, rotation, multipack and content-hashed names.
  • Sounds: every {snd} directory has its .wav files encoded to mp3 with the fx or voice preset.
  • Fonts: every {fnt} directory has its .ttf/.otf files converted to .woff2, with {weight=700} and {italic} on a file name to mark a variant.
  • For every sprites and sounds directory a typed sprites-<name>.ts / sounds-<name>.ts is generated, in the shape @studiokloek/ts-core-lib and kloek-core load. A fonts directory gets a fonts-<name>.ts with its families and a fonts-<name>.css with the @font-face rules.

Install

pnpm add -D @studiokloek/vite-assets-plugin

Node 22.22 or newer. sharp, ffmpeg-static and @napi-rs/woff-build ship native binaries; with pnpm 10+ approve their build scripts once (pnpm approve-builds).

Usage

As a Vite plugin

// vite.config.ts
import { defineConfig } from 'vite';
import { kloekAssets } from '@studiokloek/vite-assets-plugin';

export default defineConfig({
  plugins: [
    kloekAssets({
      output: { directory: 'public/assets' },
      scriptDirectory: 'src/assets',
      sounds: { prefixes: ['sj25', 'sj25_pz'] },
    }),
  ],
});

The plugin packs everything in buildStart, so the generated code exists before Vite resolves your imports, and keeps watching the entry directory while the dev server runs. The sheets are compressed (palette png, mozjpeg) in development too, so you see what production ships; set compress: false to write them fast and lossless.

As a CLI

kloek-assets                  # pack everything (development mode)
kloek-assets --mode production
kloek-assets --watch          # keep running and repack on changes (the config file itself is not watched)
kloek-assets --force          # ignore the cache
kloek-assets --no-cache
kloek-assets unused           # report sprites, sounds and fonts no code refers to
kloek-assets unused --search src --search lib
kloek-assets clean            # remove the cache directory

The CLI reads kloek-assets.config.{ts,js,mjs,json} from the working directory, or the kloekAssets key of package.json, or --config <file>.

// kloek-assets.config.ts
import { defineConfig } from '@studiokloek/vite-assets-plugin';

export default defineConfig({
  output: { directory: 'source/static' },
  scriptDirectory: 'source/script',
  sprites: { maxSize: 4096 },
});

Directory tags

Everything below entry (default assets) is walked. A directory name can carry {tag} and {tag=value} options; the tags are stripped from every name and path that ends up in the output. Several tags fit in one pair of braces, joined by a plus: menu{snd+sheet+voice} is the same as menu{snd}{sheet}{voice}, and tiles{spr+jpg+max=1024+res=1,2} works too.

assets/
  mutsop/
    sprites/
      menu{spr}/                 → one atlas "mutsop/menu"
      game/background{spr}{jpg}/ → jpg atlas "mutsop/game/background"
    sounds/
      menu{snd}{voice}/          → voice preset
      game{snd}/                 → fx preset
  generic/
    fonts{fnt}/                  → one fonts item "generic"
      Bungee.ttf                 → fonts/generic/bungee.woff2
      Bungee{weight=700}.ttf     → fonts/generic/bungee-700.woff2, the same family

| Tag | Applies to | Meaning | Default | |---|---|---|---| | {spr} | sprites | the directory is an atlas: everything below it is packed into one spritesheet | | | {jpg} / {png} | sprites | sheet format; {jpg=60} / {png=60} also sets the compression quality (1–100) of this directory | sprites.format (png), quality of sprites.compression | | {notrim} | sprites | do not trim transparent borders | sprites.trim (true) | | {extrude} / {extrude=N} | sprites | extrude sprites by N pixels (1 without a value) | sprites.extrude (0) | | {max=N} | sprites | maximum sheet size | sprites.maxSize (2048) | | {res=1,2} | sprites | resolutions to write; 2 gets the @2x suffix | sprites.resolutions ([1, 2]) | | {src=N} | sprites | resolution the sources were drawn at | sprites.sourceResolution (2) | | {meta=full\|size\|none} | sprites | per-sprite info in the generated code | sprites.metadata (full) | | {nohash} | sprites | no content hash in the output names | sprites.hash (true) | | {frames} | sprites | frame rectangles in the generated code, for loaders that do not read the json | sprites.frames (false) | | {snd} | sounds | the directory is a sound set: every .wav below it is encoded | | | {fx} / {voice} | sounds | preset: stereo, or mono at half the bitrate | sounds.preset (fx) | | {low} / {normal} / {high} | sounds | quality (bitrate) | sounds.quality (normal) | | {sheet} / {sheet=N} | sounds | join the short sounds into sheets of about N seconds, see Sound sheets; needs a loader that reads sheet and start | off, sounds.sheet.maxDuration (60) | | {fnt} | fonts | the directory is a font set: every .ttf/.otf below it becomes a .woff2, files with the same name are variants of one family | | | {weight=N} | fonts | font-weight (1–1000) of the font; on a file name, or on the directory for every font in it | 400 | | {italic} | fonts | the font is italic; on a file name, or on the directory for every font in it | normal | | {codeonly} | sprites, sounds | only generate code; the output is final and comes from elsewhere | | | {name=N} | all | trailing path parts used in the exported name | nameParts (2) | | {ignore} | all | skip the directory and everything below it | |

Rules:

  • Option tags on a parent directory apply to every item below it; a tag on the item itself wins. {spr}, {snd} and {fnt} never inherit.
  • A {spr}, {snd} or {fnt} directory inside another one is its own item and is left out of the enclosing one.
  • Fonts carry their variant on the file name: KG-Primary{weight=700}{italic}.ttf. The family is the file name without tags, so KG-Primary.ttf and KG-Primary{weight=700}.ttf are one family with two variants. A font outside a {fnt} directory is skipped with a warning.
  • With typeFolders (default) the segments sprites, sounds and fonts are dropped from item paths: mutsop/sprites/menu{spr} is the item mutsop/menu.
  • Tags on directories below an item configure nothing; they are stripped with a warning.
  • A tag without a value is not the same as leaving it out: {extrude} is {extrude=1}, while a directory without the tag gets sprites.extrude (0, no extrusion).

Output

<output>/sprites/<item path>-<hash>/<name>-<hash>-<n>[@2x].{json,png|jpg}
<output>/sounds/<hash>.mp3                  a sound, or a sheet of sounds with {sheet}
<output>/fonts/<item path>/<slug>[-<weight>][-italic].woff2   -<weight> only when it is not 400
<scriptDirectory>/<parents>/assets/{sprites,sounds,fonts}-<name>.ts
<scriptDirectory>/<parents>/assets/fonts-<name>.css

The spritesheet json is the PixiJS format (frames, meta.scale, meta.related_multi_packs, meta.number_of_packs), loadable by PixiJS 4 through 8. The generated code:

export const WorldDuinSprites = {
  eiland: { STREEPJES: { id: 'world/duin/eiland/streepjes', width: 41, height: 34 } },
};
export const WorldDuinSpritesLoaderInfo = {
  assets: WorldDuinSprites,
  fileName: 'world/duin-f9025187d7',
  numberOfParts: 1,
  type: 'sprites',
  bundle: 'world/duin',
  sources: [
    {
      part: 1,
      alias: 'world/duin-f9025187d7-1',
      variants: [
        { resolution: 1, format: 'png', json: 'sprites/world/duin-f9025187d7/duin-f9025187d7-1.json', image: 'sprites/world/duin-f9025187d7/duin-f9025187d7-1.png' },
        { resolution: 2, format: 'png', json: 'sprites/world/duin-f9025187d7/[email protected]', image: 'sprites/world/duin-f9025187d7/[email protected]' },
      ],
    },
  ],
};

export const GenericFxSounds = { SWISH_1: { id: 'eb8779673e', duration: 0.5724, name: 'swish_1' } };
export const GenericFxSoundsLoaderInfo = { assetName: 'generic/fx', assets: GenericFxSounds, numberOfSounds: 1, type: 'sounds', bundle: 'generic/fx' };

Sprite ids are identical to what pixi-texturepacker produced and sound hashes to what sounds-packer 2 produced (a project still on sounds-packer 1 gets new sound ids once), and so are assets, fileName, numberOfParts, assetName, numberOfSounds and type.

bundle and sources describe the files themselves, as plain data for any loader:

  • bundle is the item path, without a hash: a stable name for the group.
  • sources has an entry per sheet. part counts from 1, and alias is a unique name for the sheet (<fileName>-<part>).
  • variants lists every resolution written for that sheet, lowest first. format is the extension of image, and json and image are relative to output.directory, so a loader only puts its base url in front.
  • A {codeonly} item lists the sheets it expects from elsewhere, under the same names.

The types are exported as SpriteSource, SpriteSourceVariant and AssetFrameData.

Fonts

A {fnt} directory generates a constant with its families and a stylesheet with an @font-face per variant:

export const GenericFonts = {
  KG_PRIMARY: {
    family: 'KG-Primary',
    variants: [
      { weight: 400, style: 'normal', url: '/fonts/generic/kg-primary.woff2' },
      { weight: 700, style: 'italic', url: '/fonts/generic/kg-primary-700-italic.woff2' },
    ],
  },
};
@font-face {
  font-family: 'KG-Primary';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/generic/kg-primary.woff2') format('woff2');
}

Import the stylesheet once (import './assets/fonts-generic.css') instead of writing @font-face rules by hand: when a font moves, the generated paths follow. url is relative to the directory served as /: Vite's publicDir when the output lies inside it, else public under the root, else the output directory itself. The family is the file name as written, so font-family: 'KG-Primary' in CSS and GenericFonts.KG_PRIMARY.family in code name the same font.

Frames in the code

With sprites.frames (or {frames} on a directory) every sprite also carries where it sits in its sheet, for loaders that draw from the image without reading the json (DOM or canvas sprites):

STREEPJES: {
  id: 'world/duin/eiland/streepjes', width: 41, height: 34,
  part: 1,
  frame: { x: 31, y: 43, w: 41, h: 34 },
  rotated: true,
  trimmed: false,
  trimmedOffset: { x: 0, y: 0 },
},
  • part is the part of the entry in sources that holds the sprite.
  • frame and trimmedOffset are in 1x units, like width and height. Multiply by the resolution of a variant for the pixels in its image; every variant has the same layout.
  • trimmedOffset is where the trimmed frame sits inside the untrimmed width × height.
  • rotated sprites are turned 90° clockwise in the sheet: they take h × w pixels there, and frame.w and frame.h are the size before rotating, as in the json.
  • With metadata: 'none' a sprite is an object with its id and the frame fields instead of a plain string.
  • A {codeonly} item gets no frames: its sheets are not packed here.

Sound sheets

With {sheet} on a {snd} directory its short sounds are joined into a few mp3 files instead of one file each, so a set of a hundred sounds loads in a handful of requests:

export const GenericFxSounds = {
  SWISH_1: { id: 'eb8779673e', duration: 0.5724, name: 'swish_1', sheet: '2c2c19885f', start: 1.013 },
};
export const GenericFxSoundsLoaderInfo = { /* as above */ sheets: ['2c2c19885f'] };
  • A sound with a sheet has no file of its own: it is the duration seconds that start at start in sounds/<sheet>.mp3. id stays the hash of the sound itself, so it is still unique and the same as without {sheet}.
  • Sounds longer than sounds.sheet.maxSoundDuration (music, long voice-overs) keep their own file and get no sheet; so does a directory with a single short sound.
  • The sounds are spread evenly, in the order of their path, over as many sheets as it takes to stay around sounds.sheet.maxDuration seconds ({sheet=90} sets it for one directory). 60 seconds is about 1 MB at the fx/normal bitrate.
  • Every sound is followed by sounds.sheet.gap seconds of silence, and the sounds are joined before encoding, so the starts are exact to the sample.
  • sheets in the loader info lists the sheets of the set. Without {sheet} the generated code is exactly what it was.
  • The loader has to know about sheet and start: one that does not looks for sounds/<id>.mp3, which is not there. With Howler it is a sprite on the sheet, new Howl({ src: 'sounds/<sheet>.mp3', sprite: { main: [start * 1000, duration * 1000] } }); load one sound of a sheet before the others, as Howler only shares a file once it is decoded.
  • Changing one sound renames the sheets of its directory, and a sound that is in two {sheet} directories is downloaded with both.

Configuration

Every option with its default:

{
  entry: 'assets',
  output: { directory: undefined /* Vite publicDir, else 'public' */, sprites: 'sprites', sounds: 'sounds', fonts: 'fonts' },
  scriptDirectory: 'src/assets',
  cacheDirectory: undefined /* node_modules/.cache/kloek-assets/<project id> */,
  typeFolders: true,
  nameParts: 2,
  sprites: {
    resolutions: [1, 2],
    sourceResolution: 2,
    maxSize: 2048,
    format: 'png',
    trim: true,
    extrude: 0,
    metadata: 'full',
    hash: true,
    frames: false,
    compression: { png: { quality: 80, dither: 1 }, jpeg: { quality: 75 } },
  },
  sounds: {
    prefixes: [],
    preset: 'fx',
    quality: 'normal',
    sheet: { maxDuration: 60, maxSoundDuration: 10, gap: 0.05 } /* seconds, for {sheet} directories */,
  },
  fonts: { enabled: true },
  compress: true /* false: fast and lossless */,
  watch: { delay: 500 },
}

sounds.prefixes are stripped from sound names. Write a prefix without its separator (sj25): it matches both sj25_ and sj25-, and a trailing separator in the config is ignored. Unused prefixes and likely missing ones are reported.

Environment: KLOEK_ASSETS_WORKERS=0 disables the worker threads, KLOEK_ASSETS_DIRECTORIES=n limits how many sprite directories pack at once, KLOEK_ASSETS_FFMPEG=<path> overrides the ffmpeg binary.

Cache and cleanup

Unknown keys in the config are an error, so a typo like scriptDir cannot silently do nothing. Tag values are checked the same way ({max=0} or {name=x} stop the run with a message).

Everything is cached in node_modules/.cache/kloek-assets/<project id> (the id is derived from the project root, so projects that share a hoisted node_modules keep separate caches): per item a key over its sources, options, mode and the package version, encoded sounds and sprite sheets as reusable blobs, and a manifest of the last successful run. An unchanged item costs a few milliseconds. The sheets of every sprites item are kept as blobs too, uncompressed and with the current compression: when the output is gone (a cleared output directory, a CI machine that restores only the cache) or compress is switched, they are written from the cache instead of packed, and png sheets are compressed again from the uncompressed blobs when only the compression settings change. To profit from this on CI, keep the cache directory between runs. After a successful run, output and generated code that no item claims any more is removed: old hashed sprite directories, sounds of a removed directory, the script of a directory that lost its tag. Nothing is removed while any item fails: a failed item claims nothing, and pruning then would take the output of its last good run.

Migrating

From pixi-texturepacker and sounds-packer (Pixi projects): remove the assets-*.json files and put the tags on the directories themselves (generic → generic{spr}, dagupdate/states/start → start{snd}, items{voice}{high} → items{snd}{voice}{high}, ui{codeonly} → ui{spr}{codeonly}). Add a config with output.directory: 'source/static' and scriptDirectory: 'source/script'. Item paths, exported names and script locations stay the same; sprite directory names get a new hash once, and so do the sound ids when the project was still on sounds-packer 1. Code and files are regenerated together, so nothing needs to change by hand.

From vite-react-config's AssetPack pipes (React projects): the directory tags already match. Move vite.assets.sounds.prefixes to sounds.prefixes, and vite.assets.sprites.quality to sprites.compression. The generated code and the output layout change to the shapes above; adapt the loaders accordingly. Turn on sprites.frames for the frame rectangles in the code, and take the image of a sprite from sources (by its part) instead of building the path. Frames are in 1x units and can be rotated.

For coding agents

llms.txt in the package root (node_modules/@studiokloek/vite-assets-plugin/llms.txt) holds the tags, the config, the output layout and the shape of the generated code in one file.

License

MIT