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

@ignifx/vite-plugin

v0.12.0

Published

ignifx Vite plugin: asset manifest generation, scene/prefab JSON validation, WASM/asset handling, HMR hooks.

Readme

@ignifx/vite-plugin

The build-time half of the ignifx asset pipeline: it generates the asset manifest, validates the JSON file formats and the WGSL shaders, injects the project settings (ignifx.settings.json and ignifx.config.ts), copies the static files extensions need, and carries asset changes to the running game over HMR.

The package depends on nothing from the engine (docs/architecture/00-overview.md §2). It produces the files @ignifx/core consumes at runtime.

Status: pre-1.0. Breaking changes ship in minor releases (CONSTITUTION.md §4.2).

Install

pnpm add -D @ignifx/vite-plugin

vite is a peer dependency (8.x).

Usage

// vite.config.ts
import { defineConfig } from "vite";
import { ignifx } from "@ignifx/vite-plugin";

export default defineConfig({
  plugins: [ignifx()],
});

There is no default export: the plugin is the named factory ignifx (coding standards §4).

What it does

| Concern | Development | Build | | -------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------- | | Manifest | served from /assets.manifest.json and virtual:ignifx/manifest | written to <outDir>/assets.manifest.json, URLs content-hashed | | Assets | served by Vite from the asset root | copied to <outDir>/assets/ with immutable-cacheable names | | Project config | import.meta.env.IGNIFX_CONFIG | same | | Validation | reported in the terminal and the browser error overlay | fails the build | | Shaders | .wgsl files are shader assets, validated with the JSON assets | same | | Extension WASM/fonts | served from /assets/<name> | copied unhashed to <outDir>/assets/<name> | | Changes | ignifx:asset-changed over HMR; a settings edit restarts the server | — | | .ignifx/ | never served: 404 in vite dev and vite preview | never copied |

.ignifx/ is editor-private

The ignifx editor keeps per-project private state in .ignifx/ at the project root, among it .ignifx/mcp.json with the token of the project's MCP server. The plugin adds .ignifx and **/.ignifx/** to server.fs.deny (merged with yours; Vite's defaults are kept), and vite dev and vite preview answer 404 to any request whose path has a .ignifx segment, however it is encoded or cased, /@fs/ and ?raw/?url/?import forms included. They also follow symbolic links: a request whose file lies in .ignifx/ once links are resolved (src/link -> ../.ignifx) gets 404 as well, and a module whose real path is there is never loaded or bundled (IGX-0556). The asset scan skips dot-prefixed directories and, with an IGX-0556 warning, any link into .ignifx/. vite build fails with IGX-0556, after removing the copies, if its output ends up with a .ignifx directory at any depth or a publicDir file that links into one. Never move .ignifx/ under public/ or point a server at it, and do not import from it.

Options

ignifx({
  assetRoot: "assets", // scanned recursively, relative to the Vite root
  publicPath: "assets/", // where hashed assets land inside build.outDir
  manifestFileName: "assets.manifest.json",
  config: undefined, // path to ignifx.config.ts; `false` disables loading
  settingsFile: undefined, // path to ignifx.settings.json; `false` disables loading
  validate: true, // header + JSON Schema validation; a failure fails the build
  schemas: undefined, // { scene?, formats?, settings? } — supplied by the game from @ignifx/core
  hashLength: 8, // hex characters kept from each asset's sha256
  scripts: "src/scripts/**/*.ts", // glob behind virtual:ignifx/scripts
});

The manifest

assets.manifest.json carries the format/formatVersion header every ignifx file format shares (docs/architecture/06-serialization-and-scene-format.md §6) and one entry per asset, sorted by address so that two builds of the same tree produce the same bytes:

{
  "format": "ignifx.manifest",
  "formatVersion": 1,
  "root": "assets",
  "entries": [
    {
      "address": "sprites/hero.png",
      "url": "/assets/sprites/hero.b7c23e74.png",
      "bytes": 10,
      "hash": "b7c23e74",
      "type": "texture",
      "groups": ["boot"],
      "meta": { "texture": { "srgb": true } }
    }
  ]
}
  • address — the path relative to the asset root, /-separated. This is what game code loads: app.assets.load("sprites/hero.png").
  • url — root-relative in development, content-hashed in a build. The hash goes before the extension, and a two-segment JSON extension stays intact: level1.scene.json becomes level1.<hash>.scene.json, so the loader still recognises the format.
  • type — derived from the extension (texture, model, scene, material, shader, environment, font, audio, json, text, binary); an unknown extension is binary.
  • groups and meta — from the .meta.json sidecar; meta is omitted when there is none.

Sidecars

A file named <asset>.meta.json next to an asset carries its group membership and per-loader import options. groups becomes the entry's groups; everything else is passed through as meta, except the keys below.

{ "groups": ["boot", "level1"], "texture": { "srgb": true, "mipMaps": true } }

| Key | Meaning | | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | groups | An array of strings; the entry's groups. | | publish | false leaves the asset out: no manifest entry in dev or in a build, no copy in dist/, no validation. true or no key publishes it. Anything else is IGX-0551. It never reaches meta. | | guid, importer, source, outputs, options | Owned by the ignifx editor (EDITOR_SIDECAR_KEYS). They describe how the asset was imported, so they never reach meta; scanAssetRoot still reports them. | | anything else | Passed through to meta unchanged, at every depth, for the loader that reads it (texture, audio, …). |

An unpublished asset is typically an editor source whose imported output ships instead, such as a 4K .png beside the .ktx2 made from it. In vite dev the file is still on disk, so a fetch by path still works; only the manifest leaves it out.

Sidecars are never listed as assets of their own. Dot-prefixed files and directories are skipped.

Virtual modules

import { manifest } from "virtual:ignifx/manifest";
import { scripts } from "virtual:ignifx/scripts";
  • virtual:ignifx/manifest exports manifest, the same object the generated JSON holds, so a game can read it without a fetch. It is invalidated whenever an asset changes in development.
  • virtual:ignifx/scripts exports scripts, every class exported by the scripts glob that declares a static typeId. The list is sorted by module path, so registration order is the same on every machine, and the modules are imported through import.meta.glob(..., { eager: true }), which puts each script file in Vite's HMR graph. The hot-reload policies (patch vs. recreate) are @ignifx/devtools' (docs/architecture/15-devtools-and-diagnostics.md §5); this package ships the channel.

Add the declarations to your project so TypeScript knows the modules:

// src/vite-env.d.ts
declare module "virtual:ignifx/manifest" {
  import type { AssetManifest } from "@ignifx/vite-plugin";
  export const manifest: AssetManifest;
}
declare module "virtual:ignifx/scripts" {
  export const scripts: readonly (abstract new (...args: never[]) => unknown)[];
}

Project config

Project settings come in two forms at the Vite root, and either, both, or neither may exist:

  • Data: ignifx.settings.json, a JSON object with one property per settings section. This is the form a tool such as the ignifx editor writes.
  • Code: ignifx.config.ts (or .mts, .js, .mjs), loaded with Vite's own config loader. It must export a plain, JSON-serializable object by default.

The plugin merges them and injects the result as import.meta.env.IGNIFX_CONFIG (docs/architecture/04-extensions.md §5). Precedence: the config file wins, a whole section at a time. A section only one form defines comes from that form; a section both define comes entirely from ignifx.config.ts, and the plugin warns that the JSON section has no effect (unless the two are equal, as when the config file spreads the JSON).

// ignifx.config.ts
import { defineConfig } from "@ignifx/vite-plugin";

export default defineConfig({
  layers: ["Default", "Ground", "Player"],
  time: { fixedDeltaTime: 1 / 60 },
  assets: { root: "./assets", preload: ["boot"] },
});

defineConfig is exported by @ignifx/vite-plugin only; the ignifx umbrella has no ignifx/config entry point. It takes any JSON object, so a config file can keep the data in ignifx.settings.json and add to it in code:

// ignifx.config.ts
import { defineConfig } from "@ignifx/vite-plugin";
import settings from "./ignifx.settings.json";

export default defineConfig({ ...settings, time: { fixedDeltaTime: 1 / 120 } });

Pass schemas: { settings }, the JSON Schema @ignifx/core's settingsJsonSchema(app) generates from every registered section, to validate the merged settings when the dev server starts and when the build runs: a misspelled key inside a section fails with IGX-0554 and the key's JSON pointer (/time/fixedDeltaTim). A section the schema does not list only warns, because the schema is a snapshot of one app's extensions and createApp reports an unregistered section itself. Without it, createApp still validates every section when the game starts (IGX-0407 for an unknown section, IGX-0408 for a bad value or key).

Validation

Every JSON file under the asset root whose name has a two-segment JSON extension — .scene.json, .prefab.json, .material.json, .atlas.json, .input.json, and the rest of the table in docs/architecture/06-serialization-and-scene-format.md §6 — must carry a format string and a positive integer formatVersion. Plain .json files are game data and are checked only if they declare a header of their own.

When schemas is supplied, documents are also validated against the matching JSON Schema. The validator implements the subset the generated schemas use — type, required, properties, additionalProperties, items, enum, const, minimum, maximum, minItems, maxItems, oneOf, anyOf, and $ref to #/$defs/* — and reports anything outside it as IGX-0653 rather than ignoring it. Annotations (title, description, default, readOnly, …) and x- extension keywords, such as the x-ignifx-step and x-ignifx-group core writes for the inspector, are skipped. There is no runtime dependency (coding standards §13).

Problems are reported with a JSON pointer:

levels/level1.scene.json /entities/0: missing required property "uid" (IGX-0652)

Shaders

Every .wgsl file under the asset root is a shader asset — *.surface.wgsl and *.post.wgsl included, because the // @ignifx pragma inside the file, not its name, declares which of the three forms it is. The hash goes before the single .wgsl extension in a build (snow.surface.wgsl → snow.surface.<hash>.wgsl), and the manifest address stays unhashed, which is what game code loads.

Each one is parsed with wgsl_reflect (MIT, zero dependencies) at buildStart and on every dev-server change, and checked against the ignifx WGSL contract. Six rules, each reported with the file and the line:

| Rule | Reports | Code | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | Syntax | the file does not parse, at the parser's line | IGX-0654 | | Form | no // @ignifx shader\|surface\|post line, or two of them | IGX-0655 | | Declared names | a shaderUniforms.x/shaderSystem.x the file never declared — engine-supplied names included, because the struct is generated from the declaration — or a sampled texture with no // @ignifx texture line | IGX-0655 | | Entry points | shader without @vertex fn mainVertex/@fragment fn mainFragment, post without mainFragment, surface with none of displace/surface/composite | IGX-0655 | | Hand-declared bindings | a var<uniform>, var<storage>, texture or sampler in the source — ignifx and Lite generate the whole bind-group layout from the declarations | IGX-0655 | | Vertex-stage sampling | textureSample anywhere the @vertex entry reaches, and any texture read the surface form's displace hook reaches | IGX-0655 |

A shader file has to list every engine uniform it reads on an // @ignifx system line, because @ignifx/core generates the shaderUniforms and shaderSystem blocks from the declaration alone: reading shaderSystem.cameraPosition without declaring it leaves the member out of the generated struct. post files are exempt — their bindings come from the post-process stack, not from a shader material.

shaders/dissolve.wgsl: line 24: calls textureSample in "mainVertex", which the vertex stage reaches;
WGSL allows textureSample in the fragment stage only — use textureSampleLevel or textureLoad (IGX-0655)

A failure fails a production build and shows in the dev server's terminal and browser error overlay, exactly as a JSON schema failure does; validate: false turns both off. Full type checking stays with the browser's WGSL compiler, which reports through app.onError. What these rules add is a file and a line: the browser compiles the source Lite generated — your file with a scene UBO, two uniform blocks, a sampler pair per texture and a VertexInput struct in front of it — so its diagnostics point at line numbers that exist in nothing you wrote.

Editing a .wgsl file announces ignifx:asset-changed like any other asset, and @ignifx/core's shader assets rebuild their material in place.

HMR

import.meta.hot?.on("ignifx:asset-changed", ({ address, kind, url }) => {
  // kind: "added" | "changed" | "removed"
});

@ignifx/core's assets service subscribes to this and fires AssetHandle.onReplaced (docs/architecture/05-assets-and-loading.md §7). Editing ignifx.config.ts or ignifx.settings.json restarts the dev server instead: the settings are injected as a define, which only a restart recomputes, and they are frozen at app construction anyway. The plugin loads and validates the edited settings first: if they fail (malformed JSON, a config file that throws, a schemas.settings violation), the server is not restarted, the game keeps its previous settings, and the IGX-0554 error goes to the terminal and the browser's error overlay.

plugin.api.whenIdle() resolves once the plugin has finished reacting to every file-system event it has seen — useful in tests and in other plugins that read the served manifest.

Extension public assets

A package in the @ignifx scope, or any package carrying the ignifx-extension keyword, may declare files that must be served verbatim:

{ "ignifx": { "assets": { "public": ["./wasm/HavokPhysics.wasm"] } } }

They are copied unhashed into the public path, because WASM loaders locate their binary by name. Two extensions publishing the same file name is an error (IGX-0553).

Each one is also listed in the manifest, at its bare file name, with the served URL — base included. That is what makes app.assets.resolveUrl("HavokPhysics.wasm") answer correctly on a page served from a sub-path: without an entry, resolveUrl falls back to a page-relative <assetRoot>/<name>, which points somewhere no file was written as soon as the document does not sit directly above the asset root, and @ignifx/physics then reports IGX-0903. A project asset whose address equals a published file name is an error (IGX-0552), because resolveUrl could not answer for both.

If the bundler emits its own copy of the same bytes — @babylonjs/havok's ESM build carries a new URL("HavokPhysics.wasm", import.meta.url) that Rollup resolves, which would ship the binary twice — the plugin repoints that reference at its own unhashed copy and drops the bundler's, so exactly one file ships and the URLs that already worked keep working.

Discovery walks node_modules from the Vite root upwards and reads only the packages installed directly in each one. Under pnpm that is the root's own dependencies/devDependencies, so an extension reached transitively — through the ignifx umbrella, for instance — publishes nothing. A project that registers physics() through the umbrella must also list @ignifx/physics in its own package.json for HavokPhysics.wasm to be copied and listed.

Babylon Lite's decoders

@ignifx/core publishes Babylon Lite's run-time decoders this way: the KTX2 decoder (babylon.ktx2Decoder.js), its transcoders (msc_basis_transcoder.js and .wasm, six uastc_*.wasm, zstddec.wasm) and the meshopt decoder (meshopt_decoder.js), 740 KB in all. Lite fetches them from cdn.babylonjs.com unless told otherwise, and a packaged game's script-src 'self' refuses that origin. With core installed directly in the project, the plugin serves the eleven files from /assets/<name> in vite dev, copies them unhashed into <outDir>/assets/, and lists each in the manifest; core's texture and model loaders read those entries and point Lite at them before the first .ktx2 texture or glTF load. Every decoder request then stays on the game's origin, which script-src 'self' 'wasm-unsafe-eval' and connect-src 'self' (@ignifx/electron's cspFor()) allow; core's copy of the Basis transcoder is patched to compile nothing from strings, so the policy needs no 'unsafe-eval'. The files are fetched only when a game loads a .ktx2 or a meshopt-compressed glTF.

A .basis texture is the exception: Lite 1.34 always fetches its transcoder from the CDN. Use .ktx2. Draco-compressed glTF is not covered yet (engine ask E9).

Error codes

The plugin runs before an App exists, so it throws its own VitePluginError with a code from the ranges docs/architecture/15-devtools-and-diagnostics.md §1 reserves. The codes are allocated from the top of each range so that the runtime codes @ignifx/core allocates from the bottom cannot collide with them.

| Code | Meaning | | ---------- | ------------------------------------------------------------------------------------------------------------------------- | | IGX-0550 | The asset root does not exist or is not a directory (a warning in the plugin, which then builds an empty manifest) | | IGX-0551 | A .meta.json sidecar is malformed | | IGX-0552 | Two files build to the same output name | | IGX-0553 | An extension's ignifx.assets.public entry is missing or ambiguous | | IGX-0554 | ignifx.config.ts or ignifx.settings.json could not be loaded, or the settings fail schemas.settings | | IGX-0555 | A plugin option is outside its domain | | IGX-0556 | A file from the editor-private .ignifx/ reached the build output or a module load, or an asset link into it was skipped | | IGX-0650 | A JSON asset is not parseable | | IGX-0651 | A format-headed file has no usable format/formatVersion header | | IGX-0652 | A document failed its JSON Schema | | IGX-0653 | A supplied schema uses a keyword this validator does not implement | | IGX-0654 | A .wgsl shader asset does not parse as WGSL | | IGX-0655 | A .wgsl shader asset breaks the ignifx WGSL contract (see Shaders) |

License

Apache-2.0 © Astrum Forge Studios