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

@eco-incorp/sauce-compiler

v2.5.0

Published

The SauceScript compiler as WebAssembly. Compiles to bytecode for the Sauce EVM and SVM engines, in the browser and in Node.

Readme

@eco-incorp/sauce-compiler

The SauceScript compiler, compiled to WebAssembly. A thin wasm-bindgen marshalling layer over the Rust compiler core, with no compiler logic of its own. The core is I/O-free, so the JS host supplies a synchronous resolver — in Node, backed by fs.

Compiles to both targets — EVM and SVM (Solana) — and runs in both environments, Node and the browser. One package ships both builds; see JS API.

Documentation

sauce.eco.com — the Sauce overview and integration quickstart. This README documents the package's JS API.

Installable by anyone; not open source. Licensed under the Eco Proprietary License 1.0, which ships in the package as LICENSE.md. Publishing to the public registry means there is no token to configure — it grants no use rights beyond that license. The source repository is private.

Versioning is semver, and which bump a breaking change gets depends on the version you are on. On 1.x and later, a breaking change to the JS API or accepted SauceScript syntax is a major bump, new language features, options and exports are minor, and fixes are patch. On a 0.x version the API is not stable — semver says anything may change — and this package's rule there is that a breaking change is a minor bump, so pin exactly. Either way, a change to what SauceScript compiles to is a compiler change rather than an API change: read the release notes for bytecode, not the version.

Install

npm install @eco-incorp/sauce-compiler

Create src/main.ts:

function main(): Uint256 { return 6n * 7n; }

Then compile it in Node:

const { readFileSync } = require("node:fs");
const { join } = require("node:path");
const { compile } = require("@eco-incorp/sauce-compiler");

const root = "./src";
const { bytecode } = compile({
  entry: "main.ts",
  target: "evm",
  // `path` is the specifier as the source wrote it, so join it against a root.
  resolve: (path) => {
    try {
      return readFileSync(join(root, path));
    } catch (error) {
      if (error.code === "ENOENT" || error.code === "ENOTDIR") return undefined;
      throw error; // an unreadable file is a failure, not an absent target arm
    }
  },
});

JS API

The package ships two builds of the same wasm and picks between them by exports condition, so the import is the same either way — what differs is whether you have to initialize it.

Node (require, or import from ESM) resolves to the CommonJS build, which loads the wasm off disk itself. Nothing to await:

const { compile } = require("@eco-incorp/sauce-compiler");

Browsers and bundlers resolve to the ESM build, whose default export initializes the wasm. Call it once before the first compile:

import init, { compile } from "@eco-incorp/sauce-compiler";

await init(); // fetches the wasm next to the module; or pass bytes/a URL explicitly

Either environment, if you would rather not care which one you are in — ready() initializes where that is needed and is a no-op where it is not:

import { ready } from "@eco-incorp/sauce-compiler/ready";

const { compile } = await ready();

Prefer this in code that runs in both, such as an app that compiles in the browser and in its Node tests. It saves branching on which build you got: TypeScript resolves only one exports condition, so the alternative is describing the other build's shape by hand and casting into it. Call it as often as you like — initialization happens once, and concurrent callers share the one attempt.

The package ships its own TypeScript types — CompileOptions, CompileResult, AccountManifest, AccountEntry, AccountRole, SauceTarget, ResolveModule, ResolvePackage, FnCacheStats — so a consumer does not declare them. That is deliberate: an entry point typed (options: any) => any pushes every consumer into hand-writing the option and result shapes to get checking back, and two consumers then hold two copies of one contract that drift. Import the types from here instead:

import { compile, type CompileOptions } from "@eco-incorp/sauce-compiler";

Either way compile is the same function. The following illustrates all options; supply your own filesystem root, package mapper and ABI JSON:

const { readFileSync } = require("node:fs");
const { join } = require("node:path");
const { compile } = require("@eco-incorp/sauce-compiler");

const { bytecode, manifest, isaRevision, entryEncoding, entrySchema } = compile({
  entry: "main.ts", // optional, default "main.js"
  target: "evm", // "evm" | "svm"
  resolve(path) {
    // required: return the module's bytes as a Uint8Array (a Node Buffer is one).
    // `path` arrives AS THE SOURCE SPELLED IT — `"./token.json"` keeps its `./`, and
    // nothing is normalized for you. Join it against a root, or strip the `./` before
    // a map lookup; a map keyed by bare filename silently answers "absent".
    // Return undefined when there is nothing at `path` — that is how the per-target
    // arm probe (`token.evm.ts` before `token.ts`) learns an arm does not exist.
    // Throw only when the path names something you could not read: a throw is a hard
    // failure and will NOT fall back to the neutral arm.
    try {
      return readFileSync(join(root, path));
    } catch (error) {
      if (error.code === "ENOENT" || error.code === "ENOTDIR") return undefined;
      throw error;
    }
  },
  resolvePackage(specifier, importer) {
    // optional: map a bare specifier ("@sauce/token") to a path resolve() accepts.
    // Return undefined to decline — the specifier is then used as a path unchanged.
    // Throw if the package is genuinely not installed.
    return mapPackage(specifier, importer);
  },
  contracts: [["ERC20", "./out/ERC20.json"]], // optional: [name, path] ABI pairs
  inlineContracts: [["Token", tokenAbiJson]], // optional: [name, abi] pairs, ABI JSON inline
  ambient: ["@sauce/token"], // optional: modules broadcast into every module, no import line
  defines: [["FEE_BPS", "30"]], // optional: [name, value] compile-time consts, value as a string
  optimize: true, // optional, default true
  compactArgs: false, // optional, default false — ABI entry arguments
  cache: true, // optional, default true — see Caching
  chain: "polygon", // optional: the chainProfiles key the entry program runs on
  chainProfiles: { polygon: { name: "Polygon", builtins: { "ctx.blobHash": "unsupported" } } }, // optional
});

Options

| Option | Type | Notes | | ---------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | target | "evm" \| "svm" | Required. | | resolve | (path) => Uint8Array \| undefined | Required. path is the specifier as written — a relative import keeps its ./, and nothing is normalized — so join it against a root rather than keying a map by bare filename. undefined/null means "nothing at this path". Anything else is a resolve error, never silently-empty source. A .json path may be either a contract ABI or a document the source imports as data (with { type: "json" }) — both are bytes here, so serve any .json the program names. | | resolvePackage | (specifier, importer) => string \| undefined | Optional. undefined/null declines, and the specifier is then used as a path unchanged. | | entry | string | Optional, default "main.js". | | contracts | [name, path][] | Optional. Contracts the host declares by path; each path is passed to resolve. Emits the same bytes as a bare import Name from "./path.json" in the source, which is the spelling to prefer when the ABI is a file the resolver already serves; the same specifier under with { type: "json" } reads the file as data instead. | | inlineContracts | [name, abi][] | Optional. Contracts whose ABI JSON is given directly rather than a path resolve must fetch, for a caller that already holds an ABI in memory. Entry module only, exactly like contracts; a name colliding with either is a compile error. | | ambient | string[] | Optional. Module specifiers broadcast into every module — their exports are callable/readable with no import line. Each resolves as an import of it would. Ambient always loses to user code: a name the module declares, imports, or binds as a contract, a JSON data import or a named type wins silently. A module has one namespace, so an occupied name blocks the broadcast whether the two are both functions, both bindings, or one of each. | | defines | [name, value][] | Optional. Compile-time parameters injected into every module as module-scope consts. Each value is a decimal or 0x-hex integer string (a define is a 256-bit word, which JS number cannot carry), with an optional trailing n and _ separators. Precedence is the opposite of ambient: a define overrides the module's own const of that name, on both channels, so an import of it reads the defined value too. It does not override a module-scope let, a declared function, a lookup table, a bound contract, a JSON data import's binding, or an import's local alias — each is a compile error naming the define — and a function-local or parameter shadows it. A name imported under its own name is left to its import, which already carries the defined value; a data import is refused instead, because a document read from a file has no declaration for the define to stand in for and applying it nowhere would leave the option a silent no-op. The value must be assignable to the declaration's type (only 0/1 reaches a boolean) and the name must be one a source file could spell. A repeated name is an error, not a last-wins override. The name is program-wide — "every module" includes packages the host did not write, so a dependency's own const of that name is replaced with no diagnostic, and one that binds the name to anything else fails the compile; pick a name nobody else would choose. | | optimize | boolean | Optional, default true. Enables optimizations including constant folding/propagation, bounded loop unrolling, scalar expression reuse, invariant tuple-read reuse and scalar helper inlining. Validation still runs when false. | | compactArgs | boolean | Optional, default false. Uses compact-v1 entry arguments instead of ABI encoding. Encode arguments using the returned entrySchema; ordinary ABI-encoded arguments cannot be substituted. | | cache | boolean | Optional, default true. The per-function IR-lowering cache — see Caching. Set false for a guaranteed-fresh compile. | | chain | string | Optional. The chain the entry program runs on: a key of chainProfiles, whose profile its builtins are compiled against. Absent with no chainProfiles, no profile applies; absent with them is a compile error, since a host that assembled a profile registry and missed its chain lookup would otherwise get the most permissive compile. A chain with no profile is a compile error; give a chain whose builtins all behave as the target's an empty profile. | | chainProfiles | Record<string, ChainProfile> | Optional. Every chain's profile of the builtins whose behaviour differs by chain, keyed by an opaque chain key (a chain id or a name). A ChainProfile is { name?: string, builtins?: { [builtin]: "unsupported" \| { constant: string } } }, keyed by a target-gated builtin's source spelling ("ctx.blobHash", "evm.create2"). "unsupported" makes a use of the builtin a compile error naming the chain (`ctx.blobHash()` is not available on Polygon); a constant (decimal or 0x-hex, as a string) compiles the call to that literal, so folding sees through it, and is only for a builtin that takes no arguments. A builtin the profile does not name compiles as it does without one, and the target is asked first. A name that is not a builtin a profile can configure is a compile error, and every profile is checked whether or not the compile uses it. The entry is compiled against chain's profile. (A program a compile lowers for another chain is to be compiled against that chain's; no construct spells that yet, which is why the option is a map.) The cache keys each function on what its chain's profile says about the builtins it reads, so a lowering is reused between two chains only where their profiles answer alike. See Chain profiles. |

Result

| Field | Type | Meaning | | --- | --- | --- | | bytecode | Uint8Array | Compiled program, without the caller's argument values. | | isaRevision | "stack-compact-v2" | Instruction-set compatibility identifier; independent of the npm package version. | | entryEncoding | "abi-v1" \| "compact-v1" | Encoding the program expects for entry arguments. | | entrySchema | Uint8Array \| undefined | Schema for encoding compact arguments; absent with default ABI encoding. | | manifest | AccountManifest \| undefined | SVM account slots and required roles, plus a remaining-accounts marker when used. undefined on EVM. | | chain | string \| undefined | The chain the bytecode was compiled for, echoed back from chain; absent when the compile named none. The same source compiles to different bytecode per chain once a profile gives a builtin a constant or refuses it, so this is what distinguishes an artifact built for one chain from one built for another. |

How a .json import is read

A bare import Name from "./x.json" binds a contract ABI. The same specifier under with { type: "json" } reads the file as data instead. Both arrive at resolve as bytes, so a resolver serves any .json the program names and does not need to tell the two apart.

with { type: "json" } is the only attribute accepted: an unknown key or value, an empty with {}, more than one attribute, an assert { … } clause, and the attribute on a .js/.ts specifier are each a compile error naming what was wrong. A config file imported without the attribute reports that it is valid JSON but not a contract ABI, rather than a missing abi field.

Caching

compile memoizes function lowerings, on by default. The key is a function's source text, the target, and a fingerprint of every external binding its lowering read — so recompiling one program under many defines reuses the functions a changed define does not move. That is the workload it exists for; a single compile pays a little and gains nothing.

The store lives in the WASM instance and persists across compile() calls. It is bounded and automatically empties when full. clearFunctionCache() empties it explicitly and resets the counters:

const { compile, clearFunctionCache, functionCacheStats } = require("@eco-incorp/sauce-compiler");

const options = {
  target: "evm",
  resolve: (path) => path === "main.js"
    ? new TextEncoder().encode("function main(): Uint256 { return 42n; }")
    : undefined,
};
compile(options);
functionCacheStats(); // { hits, misses, inserts } — this instance's counters, not the process's

// Release cached lowerings and reset the counters, for example before measuring a cold compile.
clearFunctionCache();

Changed source or consulted bindings invalidate reuse automatically. If your resolver caches file contents, refresh that cache yourself; clearFunctionCache() cannot change the bytes it returns. cache: false bypasses lowering-cache lookup and insertion for that compile.

Errors

A validation, resolve or compile failure throws a CompileError — a JS Error whose message is the rendered report: the rule that was broken, the source line, and a caret under the span. Option and resolver errors may have no source span. A host callback's own message is threaded into the chain, so a resolver that failed says so rather than surfacing as a generic compile error.

error.diagnostics places each source error the report carries, in source order, so a host can point at the source without parsing message:

| Field | Type | Meaning | | ----------- | ------------------ | ------- | | path | string | The module the error is in, as resolve was asked for it — an error in an imported module names that module, not the entry. | | message | string | The error, as the rendered snippet titles it. | | start | SourcePosition | The first character of the span. | | end | SourcePosition | One past the span's last character. | | secondary | DiagnosticLabel? | A second location with its own note, such as where a redeclared name was first declared. |

A SourcePosition is { offset, line, column }, counted in UTF-16 code units — the unit a JavaScript string indexes in — so source.slice(d.start.offset, d.end.offset) is the span. line and column are 1-based. diagnostics is an empty array for an error with no source location.

column counts UTF-16 code units; the --> path:line:col header inside message counts characters, so the two differ on a line containing an astral character. Drive an editor from diagnostics, which is in the unit String.prototype.slice uses.

try {
  compile({ target: "evm", resolve });
} catch (error) {
  for (const d of error.diagnostics) {
    console.log(`${d.path}:${d.start.line}:${d.start.column}: ${d.message}`);
  }
}