Initial Commit
This commit is contained in:
79
packages/money/finance/currency-provider-coingecko/README.md
Normal file
79
packages/money/finance/currency-provider-coingecko/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# @suaveplan/currency-provider-coingecko - Universal CoinGecko sibling for @suaveplan/currency-providers. Exposes CoinGeckoProvider (free public tier + optional Pro-tier x-cg-pro-api-key) and the getCoinGeckoId symbol lookup; routes every request through the shared httpRequest helper and raises typed CurrencyProviderError on HTTP_ERROR / PARSE_ERROR / UPSTREAM_ERROR / UNKNOWN_CURRENCY.
|
||||
|
||||
Universal CoinGecko sibling for
|
||||
[`@suaveplan/currency-providers`](../currency-providers/README.md). Implements
|
||||
the `CurrencyProvider` contract from `@suaveplan/currency` against the
|
||||
public `/simple/price` endpoint, with optional Pro-tier `apiKey` support
|
||||
attached as `x-cg-pro-api-key` on every outbound request. Both free and
|
||||
Pro tiers share the same URL shape, so upgrading is a single-field
|
||||
config change.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [coin-ids](./src/coin-ids/coin-ids.md) — Owns the curated mapping from uppercase ticker symbol (`BTC`, `ETH`, …) to the opaque CoinGecko coin id (`bitcoin`, `et…
|
||||
- [coin-ids](./src/coin-ids/coin-ids.md) — Owns the curated mapping from uppercase ticker symbol (`BTC`, `ETH`, …) to the opaque CoinGecko coin id (`bitcoin`, `et…
|
||||
- [coingecko](./src/coingecko/coingecko.md) — CoinGecko sibling for `@suaveplan/currency-providers`. Implements the `CurrencyProvider` contract from `@suaveplan/curr…
|
||||
- [coingecko](./src/coingecko/coingecko.md) — CoinGecko sibling for `@suaveplan/currency-providers`. Implements the `CurrencyProvider` contract from `@suaveplan/curr…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/currency-provider-coingecko
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { CurrencyConverter } from "@suaveplan/currency";
|
||||
import { CurrencyProviderRegistry } from "@suaveplan/currency-providers";
|
||||
import { CoinGeckoProvider } from "@suaveplan/currency-provider-coingecko";
|
||||
|
||||
// Free public tier — no API key required.
|
||||
const free = new CoinGeckoProvider();
|
||||
|
||||
// Pro tier — pass apiKey; the same provider attaches x-cg-pro-api-key.
|
||||
const pro = new CoinGeckoProvider({
|
||||
apiKey: process.env.COINGECKO_PRO_KEY,
|
||||
baseUrl: "https://pro-api.coingecko.com/api/v3",
|
||||
});
|
||||
|
||||
const registry = new CurrencyProviderRegistry();
|
||||
registry.register(pro);
|
||||
|
||||
const converter = new CurrencyConverter({
|
||||
provider: registry.get("coingecko"),
|
||||
staleDurationMs: 60_000,
|
||||
});
|
||||
|
||||
const rates = await registry
|
||||
.get("coingecko")
|
||||
.fetchRates({ base: "USD", targets: ["BTC", "ETH"] });
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- `CoinGeckoProvider implements CurrencyProvider` — drops directly into
|
||||
any `@suaveplan/currency` converter or `CurrencyProviderRegistry`.
|
||||
- Truly optional `apiKey` — free tier requires no config; Pro tier
|
||||
attaches `x-cg-pro-api-key` automatically.
|
||||
- 16 curated cryptocurrencies (BTC, ETH, SOL, USDT, USDC, BNB, XRP, ADA,
|
||||
DOGE, DOT, AVAX, MATIC, LINK, UNI, ATOM, LTC).
|
||||
- Routes every request through the shared `httpRequest()` helper from
|
||||
`@suaveplan/currency-providers` for abort-signal composition,
|
||||
per-call timeouts, and typed HTTP failures.
|
||||
- Proactive `UNKNOWN_CURRENCY` gate — unknown `options.targets` reject
|
||||
before the HTTP call is issued.
|
||||
- Typed `CurrencyProviderError` on every failure path: `HTTP_ERROR`,
|
||||
`PARSE_ERROR`, `UPSTREAM_ERROR`, `UNKNOWN_CURRENCY`.
|
||||
- Universal — zero browser / Node-only APIs. Works in Node 18+, Bun,
|
||||
modern browsers, and Web Workers.
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/coingecko/coingecko.md`](./src/coingecko/coingecko.md) —
|
||||
`CoinGeckoProvider` class + `CoinGeckoConfig` type.
|
||||
- [`src/coin-ids/coin-ids.md`](./src/coin-ids/coin-ids.md) — `COIN_IDS`
|
||||
curated map + `getCoinGeckoId(symbol)` lookup.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "@suaveplan/currency-provider-coingecko",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"description": "Universal CoinGecko sibling for @suaveplan/currency-providers. Exposes CoinGeckoProvider (free public tier + optional Pro-tier x-cg-pro-api-key) and the getCoinGeckoId symbol lookup; routes every request through the shared httpRequest helper and raises typed CurrencyProviderError on HTTP_ERROR / PARSE_ERROR / UPSTREAM_ERROR / UNKNOWN_CURRENCY.",
|
||||
"license": "MIT",
|
||||
"author": "Suaveplan Team",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./coingecko/coingecko": {
|
||||
"import": {
|
||||
"types": "./dist/coingecko/coingecko.d.ts",
|
||||
"default": "./dist/coingecko/coingecko.js"
|
||||
}
|
||||
},
|
||||
"./coin-ids/coin-ids": {
|
||||
"import": {
|
||||
"types": "./dist/coin-ids/coin-ids.d.ts",
|
||||
"default": "./dist/coin-ids/coin-ids.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage --passWithNoTests",
|
||||
"lint": "bunx biome check src/",
|
||||
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
|
||||
"format": "bunx biome format --write --max-diagnostics 500 src/",
|
||||
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
|
||||
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
|
||||
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@suaveplan/currency": "workspace:^",
|
||||
"@suaveplan/currency-providers": "workspace:^",
|
||||
"@suaveplan/error": "workspace:^",
|
||||
"@suaveplan/types": "workspace:^",
|
||||
"tslib": "catalog:build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@suaveplan/testing": "workspace:^",
|
||||
"@types/node": "catalog:",
|
||||
"@vitest/coverage-istanbul": "catalog:testing",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:build",
|
||||
"vite-plugin-dts": "catalog:build",
|
||||
"vitest": "catalog:testing"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npmjs.fq.io/",
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
# coin-ids
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Owns the curated mapping from uppercase ticker symbol (`BTC`, `ETH`, …) to
|
||||
the opaque CoinGecko coin id (`bitcoin`, `ethereum`, …) that the
|
||||
`/simple/price` endpoint requires. Without this translation step the
|
||||
provider would have to either fall back to CoinGecko's heavyweight
|
||||
`/coins/list` discovery endpoint (~10k entries, defeats tree-shaking) or
|
||||
ask every caller to learn CoinGecko's id convention — neither is
|
||||
acceptable for a slim sibling.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `COIN_IDS` map covering the top 16 cryptos by market cap at
|
||||
landing (BTC, ETH, SOL, USDT, USDC, BNB, XRP, ADA, DOGE, DOT, AVAX,
|
||||
MATIC, LINK, UNI, ATOM, LTC).
|
||||
- Case-insensitive `getCoinGeckoId(symbol)` lookup returning `string | undefined`.
|
||||
- Zero runtime dependencies; pure data + a one-line lookup function.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { getCoinGeckoId } from "@suaveplan/currency-provider-coingecko/coin-ids/coin-ids";
|
||||
|
||||
getCoinGeckoId("BTC"); // "bitcoin"
|
||||
getCoinGeckoId("eth"); // "ethereum" — case-insensitive
|
||||
getCoinGeckoId("ZZZ"); // undefined — unknown symbol
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
The provider uses `getCoinGeckoId` as the proactive `UNKNOWN_CURRENCY`
|
||||
gate. Consumers can mirror the same pattern when composing their own
|
||||
filters before hitting the registry:
|
||||
|
||||
```ts
|
||||
import {
|
||||
COIN_IDS,
|
||||
getCoinGeckoId,
|
||||
} from "@suaveplan/currency-provider-coingecko/coin-ids/coin-ids";
|
||||
|
||||
function partitionSupported(symbols: readonly string[]): {
|
||||
supported: string[];
|
||||
unknown: string[];
|
||||
} {
|
||||
const supported: string[] = [];
|
||||
const unknown: string[] = [];
|
||||
for (const symbol of symbols) {
|
||||
if (getCoinGeckoId(symbol) !== undefined) {
|
||||
supported.push(symbol);
|
||||
} else {
|
||||
unknown.push(symbol);
|
||||
}
|
||||
}
|
||||
return { supported, unknown };
|
||||
}
|
||||
|
||||
// Iterate the full map when generating a UI dropdown:
|
||||
const everyKnown = Object.keys(COIN_IDS);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `COIN_IDS: Readonly<Record<string, string>>`
|
||||
|
||||
Frozen mapping from uppercase ticker (key) to CoinGecko coin id (value).
|
||||
Keys are always uppercase; mutation throws because the object is frozen.
|
||||
|
||||
### `getCoinGeckoId(symbol: string): string | undefined`
|
||||
|
||||
Returns the CoinGecko coin id for the given symbol. The input is
|
||||
upper-cased before the lookup, so `"btc"` and `"BTC"` both resolve to
|
||||
`"bitcoin"`. Returns `undefined` for any symbol not present in
|
||||
`COIN_IDS`. Does not throw.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The map is intentionally hand-curated rather than fetched at runtime: a
|
||||
sibling that issues a discovery call before every batch would (a) add a
|
||||
network round trip every consumer pays for once, (b) bloat the
|
||||
in-memory cache, and (c) couple this package's reliability to a second
|
||||
upstream endpoint whose response shape can drift. Adding a new symbol is
|
||||
a one-line PR; the trade-off is heavily skewed toward "type it in".
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { COIN_IDS, getCoinGeckoId } from "./coin-ids.js";
|
||||
|
||||
describe("getCoinGeckoId", () => {
|
||||
it("returns the CoinGecko id for known symbols regardless of case", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(getCoinGeckoId("BTC")).toBe("bitcoin");
|
||||
expect(getCoinGeckoId("btc")).toBe("bitcoin");
|
||||
expect(getCoinGeckoId("Eth")).toBe("ethereum");
|
||||
expect(getCoinGeckoId("SOL")).toBe("solana");
|
||||
expect(getCoinGeckoId("matic")).toBe("matic-network");
|
||||
});
|
||||
|
||||
it("returns undefined for unknown symbols", { timeout: 5000 }, () => {
|
||||
expect(getCoinGeckoId("ZZZ")).toBeUndefined();
|
||||
expect(getCoinGeckoId("")).toBeUndefined();
|
||||
expect(getCoinGeckoId("not-a-symbol")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("COIN_IDS", () => {
|
||||
it("covers the top-16 curated set with uppercase keys", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const expectedKeys = [
|
||||
"BTC",
|
||||
"ETH",
|
||||
"SOL",
|
||||
"USDT",
|
||||
"USDC",
|
||||
"BNB",
|
||||
"XRP",
|
||||
"ADA",
|
||||
"DOGE",
|
||||
"DOT",
|
||||
"AVAX",
|
||||
"MATIC",
|
||||
"LINK",
|
||||
"UNI",
|
||||
"ATOM",
|
||||
"LTC",
|
||||
];
|
||||
expect(Object.keys(COIN_IDS).sort()).toEqual(expectedKeys.sort());
|
||||
for (const key of Object.keys(COIN_IDS)) {
|
||||
expect(key).toBe(key.toUpperCase());
|
||||
}
|
||||
});
|
||||
|
||||
it("is frozen — runtime mutation is rejected", { timeout: 5000 }, () => {
|
||||
expect(Object.isFrozen(COIN_IDS)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Curated mapping from uppercase crypto symbol to the CoinGecko coin id
|
||||
* required by the `/simple/price` endpoint. CoinGecko's API is keyed by an
|
||||
* opaque coin id (`bitcoin`, `ethereum`, …) rather than the ticker symbol,
|
||||
* so every request the provider issues SHALL pass values from this map.
|
||||
*
|
||||
* The set covers the top 16 cryptocurrencies by market cap at landing time.
|
||||
* Consumers needing additional symbols register them via their own wrapper
|
||||
* provider; we deliberately do not auto-fetch CoinGecko's `/coins/list`
|
||||
* because that endpoint is heavy (~10k entries) and the resulting bundle
|
||||
* would defeat the purpose of having a slim sibling.
|
||||
*/
|
||||
export const COIN_IDS: Readonly<Record<string, string>> = Object.freeze({
|
||||
BTC: "bitcoin",
|
||||
ETH: "ethereum",
|
||||
SOL: "solana",
|
||||
USDT: "tether",
|
||||
USDC: "usd-coin",
|
||||
BNB: "binancecoin",
|
||||
XRP: "ripple",
|
||||
ADA: "cardano",
|
||||
DOGE: "dogecoin",
|
||||
DOT: "polkadot",
|
||||
AVAX: "avalanche-2",
|
||||
MATIC: "matic-network",
|
||||
LINK: "chainlink",
|
||||
UNI: "uniswap",
|
||||
ATOM: "cosmos",
|
||||
LTC: "litecoin",
|
||||
});
|
||||
|
||||
/**
|
||||
* Case-insensitive lookup of the CoinGecko coin id for a ticker symbol.
|
||||
* Returns `undefined` when the symbol is not present in the curated map;
|
||||
* callers that issue requests SHOULD treat that result as an
|
||||
* `UNKNOWN_CURRENCY` condition rather than passing the raw symbol upstream.
|
||||
*/
|
||||
export function getCoinGeckoId(symbol: string): string | undefined {
|
||||
return COIN_IDS[symbol.toUpperCase()];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
COIN_IDS,
|
||||
getCoinGeckoId,
|
||||
} from "./coin-ids.js";
|
||||
@@ -0,0 +1,114 @@
|
||||
# CoinGeckoProvider
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
CoinGecko sibling for `@suaveplan/currency-providers`. Implements the
|
||||
`CurrencyProvider` contract from `@suaveplan/currency` against the
|
||||
public `/simple/price` endpoint, with optional Pro-tier `apiKey`
|
||||
support that attaches the `x-cg-pro-api-key` header to every outbound
|
||||
request. The free public tier (no key, 10–30 calls/min) and the Pro
|
||||
tier share the same URL shape, so upgrading from free to Pro is a
|
||||
single-field config change with no other wiring impact.
|
||||
|
||||
## Features
|
||||
|
||||
- `CoinGeckoProvider implements CurrencyProvider` — `name = "coingecko"`,
|
||||
`supportsCrypto = true`, `supportsFiat = true`.
|
||||
- Optional `apiKey` config (`x-cg-pro-api-key` header attachment).
|
||||
- Optional `baseUrl` and `fetch` config (default
|
||||
`https://api.coingecko.com/api/v3` and `globalThis.fetch`).
|
||||
- Caller `AbortSignal` propagation via the shared `httpRequest()` helper
|
||||
(composes with timeouts from the helper).
|
||||
- Proactive `UNKNOWN_CURRENCY` gate — unknown `options.targets` reject
|
||||
before the HTTP call is issued.
|
||||
- Typed `CurrencyProviderError` on every failure path: `HTTP_ERROR`,
|
||||
`PARSE_ERROR`, `UPSTREAM_ERROR`, `UNKNOWN_CURRENCY`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { CoinGeckoProvider } from "@suaveplan/currency-provider-coingecko/coingecko/coingecko";
|
||||
|
||||
const provider = new CoinGeckoProvider();
|
||||
const result = await provider.fetchRates({ base: "USD" });
|
||||
// result.rates.BTC = 0.0000149… (1 USD ≈ 0.0000149 BTC at ~$67k/BTC)
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { CurrencyConverter } from "@suaveplan/currency";
|
||||
import { CurrencyProviderRegistry } from "@suaveplan/currency-providers";
|
||||
import { CoinGeckoProvider } from "@suaveplan/currency-provider-coingecko/coingecko/coingecko";
|
||||
|
||||
const registry = new CurrencyProviderRegistry();
|
||||
registry.register(
|
||||
new CoinGeckoProvider({
|
||||
apiKey: process.env.COINGECKO_PRO_KEY,
|
||||
baseUrl: "https://pro-api.coingecko.com/api/v3",
|
||||
}),
|
||||
);
|
||||
|
||||
const converter = new CurrencyConverter({
|
||||
provider: registry.get("coingecko"),
|
||||
staleDurationMs: 60_000, // 1 minute — crypto rates move fast
|
||||
});
|
||||
|
||||
// Targets are validated against the curated COIN_IDS map BEFORE the
|
||||
// HTTP call — passing an unknown target raises UNKNOWN_CURRENCY
|
||||
// without making a network round trip.
|
||||
const controller = new AbortController();
|
||||
const rates = await registry.get("coingecko").fetchRates({
|
||||
base: "EUR",
|
||||
targets: ["BTC", "ETH"],
|
||||
signal: controller.signal,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class CoinGeckoProvider implements CurrencyProvider`
|
||||
|
||||
- `constructor(config?: CoinGeckoConfig)` — config is fully optional.
|
||||
- `readonly name: "coingecko"`
|
||||
- `readonly supportsCrypto: true`
|
||||
- `readonly supportsFiat: true`
|
||||
- `fetchRates(options?: RateFetchOptions): Promise<RateFetchResult>`
|
||||
- Issues `GET ${baseUrl}/simple/price?ids=<curated-ids>&vs_currencies=<base.toLowerCase()>`.
|
||||
- Forwards `options.signal` to the shared `httpRequest()` helper.
|
||||
- Pre-validates `options.targets` against `COIN_IDS`; an unknown entry
|
||||
raises `CurrencyProviderError(UNKNOWN_CURRENCY)` before any network
|
||||
activity.
|
||||
- Inverts upstream "1 crypto = X fiat" prices into the converter's
|
||||
"1 base = X crypto" convention.
|
||||
- Throws `CurrencyProviderError` with `code` ∈ {`HTTP_ERROR`,
|
||||
`PARSE_ERROR`, `UPSTREAM_ERROR`, `UNKNOWN_CURRENCY`} carrying
|
||||
`providerName === "coingecko"`.
|
||||
|
||||
### `interface CoinGeckoConfig extends NoKeyConfig`
|
||||
|
||||
- `readonly baseUrl?: string` — overrides `https://api.coingecko.com/api/v3`.
|
||||
- `readonly fetch?: FetchLike` — overrides `globalThis.fetch`.
|
||||
- `readonly apiKey?: string` — Pro-tier key, attached as
|
||||
`x-cg-pro-api-key` header when present.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Pre-validating `options.targets` against `COIN_IDS` (not against the
|
||||
upstream response) means the rejection message names the bad symbol
|
||||
rather than swallowing it with a missing-key in the result map. This
|
||||
is the difference between "your `ZZZ` symbol is not in our curated
|
||||
set" (actionable) and "your rate result has no `ZZZ` field" (silent
|
||||
defect).
|
||||
- The shape guard (`isResponseShape`) accepts any value matching
|
||||
`Record<string, Record<string, number>>` rather than asserting the
|
||||
exact `bitcoin`/`ethereum` keys CoinGecko currently returns. The
|
||||
upstream may add new coins or rename old ones, and a strict assertion
|
||||
would treat that as a wire-format break instead of a benign superset.
|
||||
- Headers are constructed once at instantiation time and frozen so the
|
||||
per-request `init` spread is the only mutable copy — defends against
|
||||
accidental key leakage into a downstream debugger.
|
||||
@@ -0,0 +1,364 @@
|
||||
import {
|
||||
CURRENCY_PROVIDER_CODES,
|
||||
CurrencyProviderError,
|
||||
} from "@suaveplan/currency-providers";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import type { FetchLike } from "@suaveplan/types";
|
||||
import { CoinGeckoProvider } from "./coingecko.js";
|
||||
|
||||
interface FetchCall {
|
||||
url: string;
|
||||
init: RequestInit | undefined;
|
||||
}
|
||||
|
||||
function recordingFetch(
|
||||
handler: (call: FetchCall) => Response | Promise<Response>
|
||||
): { fetch: FetchLike; calls: FetchCall[] } {
|
||||
const calls: FetchCall[] = [];
|
||||
const fetchImpl: FetchLike = async (
|
||||
url: string | URL | Request,
|
||||
init?: RequestInit
|
||||
): Promise<Response> => {
|
||||
const call: FetchCall = { url: String(url), init };
|
||||
calls.push(call);
|
||||
return await handler(call);
|
||||
};
|
||||
return { fetch: fetchImpl, calls };
|
||||
}
|
||||
|
||||
const USD_BODY = {
|
||||
bitcoin: { usd: 67000 },
|
||||
ethereum: { usd: 3500 },
|
||||
solana: { usd: 150 },
|
||||
tether: { usd: 1 },
|
||||
"usd-coin": { usd: 1 },
|
||||
};
|
||||
|
||||
function ok(body: unknown): Response {
|
||||
return new Response(JSON.stringify(body), { status: 200 });
|
||||
}
|
||||
|
||||
describe("CoinGeckoProvider", () => {
|
||||
it("exposes stable name and supports flags", { timeout: 5000 }, () => {
|
||||
const provider = new CoinGeckoProvider();
|
||||
expect(provider.name).toBe("coingecko");
|
||||
expect(provider.supportsCrypto).toBe(true);
|
||||
expect(provider.supportsFiat).toBe(true);
|
||||
});
|
||||
|
||||
it("issues request to default endpoint with vs_currencies from base", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { fetch, calls } = recordingFetch(() => ok(USD_BODY));
|
||||
const provider = new CoinGeckoProvider({ fetch });
|
||||
|
||||
await provider.fetchRates({ base: "USD" });
|
||||
|
||||
const url = calls[0]?.url ?? "";
|
||||
expect(
|
||||
url.startsWith("https://api.coingecko.com/api/v3/simple/price?ids=")
|
||||
).toBe(true);
|
||||
expect(url.endsWith("&vs_currencies=usd")).toBe(true);
|
||||
});
|
||||
|
||||
it("honours custom baseUrl override", { timeout: 5000 }, async () => {
|
||||
const { fetch, calls } = recordingFetch(() =>
|
||||
ok({ bitcoin: { eur: 62000 } })
|
||||
);
|
||||
const provider = new CoinGeckoProvider({
|
||||
baseUrl: "https://custom.example/v3",
|
||||
fetch,
|
||||
});
|
||||
|
||||
await provider.fetchRates({ base: "EUR" });
|
||||
|
||||
expect(
|
||||
calls[0]?.url.startsWith("https://custom.example/v3/simple/price")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("attaches x-cg-pro-api-key header when apiKey is provided", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { fetch, calls } = recordingFetch(() => ok(USD_BODY));
|
||||
const provider = new CoinGeckoProvider({ apiKey: "secret", fetch });
|
||||
|
||||
await provider.fetchRates();
|
||||
|
||||
const headers = calls[0]?.init?.headers as
|
||||
| Record<string, string>
|
||||
| undefined;
|
||||
expect(headers).toBeDefined();
|
||||
expect(headers?.["x-cg-pro-api-key"]).toBe("secret");
|
||||
});
|
||||
|
||||
it("omits x-cg-pro-api-key header when apiKey is absent", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { fetch, calls } = recordingFetch(() => ok(USD_BODY));
|
||||
const provider = new CoinGeckoProvider({ fetch });
|
||||
|
||||
await provider.fetchRates();
|
||||
|
||||
// init.headers should not exist at all on the keyless branch
|
||||
expect(calls[0]?.init?.headers).toBeUndefined();
|
||||
});
|
||||
|
||||
it("inverts upstream prices into base→crypto rates", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { fetch } = recordingFetch(() => ok(USD_BODY));
|
||||
const provider = new CoinGeckoProvider({ fetch });
|
||||
|
||||
const result = await provider.fetchRates({ base: "USD" });
|
||||
|
||||
expect(result.base).toBe("USD");
|
||||
expect(result.rates.BTC).toBeCloseTo(1 / 67000, 12);
|
||||
expect(result.rates.ETH).toBeCloseTo(1 / 3500, 12);
|
||||
expect(result.rates.SOL).toBeCloseTo(1 / 150, 12);
|
||||
expect(result.timestamp).toBeTypeOf("number");
|
||||
});
|
||||
|
||||
it("skips known targets whose upstream price is absent", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
// ATOM is a known symbol (passes UNKNOWN_CURRENCY gate) but
|
||||
// upstream returned only bitcoin — the filter branch where
|
||||
// allRates[target] is undefined must be exercised.
|
||||
const { fetch } = recordingFetch(() => ok({ bitcoin: { usd: 67000 } }));
|
||||
const provider = new CoinGeckoProvider({ fetch });
|
||||
|
||||
const result = await provider.fetchRates({
|
||||
base: "USD",
|
||||
targets: ["BTC", "ATOM"],
|
||||
});
|
||||
|
||||
expect(result.rates.BTC).toBeCloseTo(1 / 67000, 12);
|
||||
expect(result.rates.ATOM).toBeUndefined();
|
||||
expect(Object.keys(result.rates)).toEqual(["BTC"]);
|
||||
});
|
||||
|
||||
it("filters rates by options.targets", { timeout: 5000 }, async () => {
|
||||
const { fetch } = recordingFetch(() => ok(USD_BODY));
|
||||
const provider = new CoinGeckoProvider({ fetch });
|
||||
|
||||
const result = await provider.fetchRates({
|
||||
base: "USD",
|
||||
targets: ["BTC", "ETH"],
|
||||
});
|
||||
|
||||
expect(Object.keys(result.rates).sort()).toEqual(["BTC", "ETH"]);
|
||||
});
|
||||
|
||||
it("omits symbols whose upstream price is missing or non-positive", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { fetch } = recordingFetch(() =>
|
||||
ok({
|
||||
bitcoin: { usd: 67000 },
|
||||
ethereum: { usd: 0 }, // non-positive — skip
|
||||
solana: {}, // missing vs_currency — skip
|
||||
"not-a-known-id": { usd: 5 }, // unknown id — skip
|
||||
})
|
||||
);
|
||||
const provider = new CoinGeckoProvider({ fetch });
|
||||
|
||||
const result = await provider.fetchRates({ base: "USD" });
|
||||
|
||||
expect(result.rates.BTC).toBeCloseTo(1 / 67000, 12);
|
||||
expect(result.rates.ETH).toBeUndefined();
|
||||
expect(result.rates.SOL).toBeUndefined();
|
||||
});
|
||||
|
||||
it("raises HTTP_ERROR on non-2xx response", { timeout: 5000 }, async () => {
|
||||
const { fetch } = recordingFetch(
|
||||
() =>
|
||||
new Response("", {
|
||||
status: 429,
|
||||
statusText: "Too Many Requests",
|
||||
})
|
||||
);
|
||||
const provider = new CoinGeckoProvider({ fetch });
|
||||
|
||||
try {
|
||||
await provider.fetchRates();
|
||||
expect.fail("fetchRates should have rejected");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(CurrencyProviderError);
|
||||
const err = error as CurrencyProviderError;
|
||||
expect(err.code).toBe(CURRENCY_PROVIDER_CODES.HTTP_ERROR);
|
||||
expect(err.providerName).toBe("coingecko");
|
||||
expect(err.status).toBe(429);
|
||||
}
|
||||
});
|
||||
|
||||
it("raises PARSE_ERROR when body is not valid JSON", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { fetch } = recordingFetch(
|
||||
() => new Response("not json at all", { status: 200 })
|
||||
);
|
||||
const provider = new CoinGeckoProvider({ fetch });
|
||||
|
||||
try {
|
||||
await provider.fetchRates();
|
||||
expect.fail("fetchRates should have rejected");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(CurrencyProviderError);
|
||||
const err = error as CurrencyProviderError;
|
||||
expect(err.code).toBe(CURRENCY_PROVIDER_CODES.PARSE_ERROR);
|
||||
expect(err.providerName).toBe("coingecko");
|
||||
expect(err.cause).toBeInstanceOf(Error);
|
||||
}
|
||||
});
|
||||
|
||||
it("raises PARSE_ERROR even when JSON parser throws a non-Error", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fetchImpl: FetchLike = async (): Promise<Response> => {
|
||||
// Build a Response-like whose .json() rejects with a non-Error throwable.
|
||||
const fake = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
json: async (): Promise<unknown> => {
|
||||
const literal: unknown = "string-throwable";
|
||||
throw literal;
|
||||
},
|
||||
};
|
||||
return fake as unknown as Response;
|
||||
};
|
||||
const provider = new CoinGeckoProvider({ fetch: fetchImpl });
|
||||
|
||||
try {
|
||||
await provider.fetchRates();
|
||||
expect.fail("fetchRates should have rejected");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(CurrencyProviderError);
|
||||
const err = error as CurrencyProviderError;
|
||||
expect(err.code).toBe(CURRENCY_PROVIDER_CODES.PARSE_ERROR);
|
||||
// cause is only attached when the thrown value was an Error
|
||||
expect(err.cause).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("raises UPSTREAM_ERROR when body is the wrong shape (array)", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { fetch } = recordingFetch(() => ok([1, 2, 3]));
|
||||
const provider = new CoinGeckoProvider({ fetch });
|
||||
|
||||
try {
|
||||
await provider.fetchRates();
|
||||
expect.fail("fetchRates should have rejected");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(CurrencyProviderError);
|
||||
const err = error as CurrencyProviderError;
|
||||
expect(err.code).toBe(CURRENCY_PROVIDER_CODES.UPSTREAM_ERROR);
|
||||
expect(err.providerName).toBe("coingecko");
|
||||
}
|
||||
});
|
||||
|
||||
it("raises UPSTREAM_ERROR when body inner value is not an object", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { fetch } = recordingFetch(() =>
|
||||
ok({ bitcoin: "not-an-object" })
|
||||
);
|
||||
const provider = new CoinGeckoProvider({ fetch });
|
||||
|
||||
await expect(provider.fetchRates()).rejects.toMatchObject({
|
||||
code: CURRENCY_PROVIDER_CODES.UPSTREAM_ERROR,
|
||||
});
|
||||
});
|
||||
|
||||
it("raises UPSTREAM_ERROR when body inner value is null", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { fetch } = recordingFetch(() => ok({ bitcoin: null }));
|
||||
const provider = new CoinGeckoProvider({ fetch });
|
||||
|
||||
await expect(provider.fetchRates()).rejects.toMatchObject({
|
||||
code: CURRENCY_PROVIDER_CODES.UPSTREAM_ERROR,
|
||||
});
|
||||
});
|
||||
|
||||
it("raises UPSTREAM_ERROR when body is null", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { fetch } = recordingFetch(() => ok(null));
|
||||
const provider = new CoinGeckoProvider({ fetch });
|
||||
|
||||
await expect(provider.fetchRates()).rejects.toMatchObject({
|
||||
code: CURRENCY_PROVIDER_CODES.UPSTREAM_ERROR,
|
||||
});
|
||||
});
|
||||
|
||||
it("raises UNKNOWN_CURRENCY when a target has no CoinGecko mapping", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { fetch, calls } = recordingFetch(() => ok(USD_BODY));
|
||||
const provider = new CoinGeckoProvider({ fetch });
|
||||
|
||||
try {
|
||||
await provider.fetchRates({ base: "USD", targets: ["BTC", "ZZZ"] });
|
||||
expect.fail("fetchRates should have rejected");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(CurrencyProviderError);
|
||||
const err = error as CurrencyProviderError;
|
||||
expect(err.code).toBe(CURRENCY_PROVIDER_CODES.UNKNOWN_CURRENCY);
|
||||
expect(err.providerName).toBe("coingecko");
|
||||
expect(err.message).toContain("ZZZ");
|
||||
}
|
||||
|
||||
// No HTTP call was issued — the gate fires before httpRequest.
|
||||
expect(calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it("forwards options.signal so caller can abort in flight", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const controller = new AbortController();
|
||||
const fetchImpl: FetchLike = async (
|
||||
_url: string | URL | Request,
|
||||
init?: RequestInit
|
||||
): Promise<Response> => {
|
||||
return new Promise<Response>((_resolve, reject) => {
|
||||
init?.signal?.addEventListener("abort", () => {
|
||||
reject(
|
||||
init?.signal?.reason ??
|
||||
new DOMException("aborted", "AbortError")
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
const provider = new CoinGeckoProvider({ fetch: fetchImpl });
|
||||
|
||||
const promise = provider.fetchRates({ signal: controller.signal });
|
||||
controller.abort(new Error("caller cancelled"));
|
||||
|
||||
await expect(promise).rejects.toThrow(/caller cancelled/);
|
||||
});
|
||||
|
||||
it("defaults to USD base and globalThis.fetch when no options/config supplied", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const original = globalThis.fetch;
|
||||
const calls: FetchCall[] = [];
|
||||
globalThis.fetch = (async (
|
||||
url: string | URL | Request,
|
||||
init?: RequestInit
|
||||
): Promise<Response> => {
|
||||
calls.push({ url: String(url), init });
|
||||
return ok(USD_BODY);
|
||||
}) as typeof globalThis.fetch;
|
||||
try {
|
||||
const provider = new CoinGeckoProvider();
|
||||
const result = await provider.fetchRates();
|
||||
expect(result.base).toBe("USD");
|
||||
expect(calls[0]?.url.endsWith("&vs_currencies=usd")).toBe(true);
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,181 @@
|
||||
import type {
|
||||
CurrencyProvider,
|
||||
RateFetchOptions,
|
||||
RateFetchResult,
|
||||
} from "@suaveplan/currency";
|
||||
import {
|
||||
CURRENCY_PROVIDER_CODES,
|
||||
CurrencyProviderError,
|
||||
httpRequest,
|
||||
type NoKeyConfig,
|
||||
} from "@suaveplan/currency-providers";
|
||||
import type { FetchLike } from "@suaveplan/types";
|
||||
import { COIN_IDS, getCoinGeckoId } from "../coin-ids/coin-ids.js";
|
||||
|
||||
const PROVIDER_NAME = "coingecko";
|
||||
const DEFAULT_BASE_URL = "https://api.coingecko.com/api/v3";
|
||||
const PRO_API_KEY_HEADER = "x-cg-pro-api-key";
|
||||
const ALL_IDS = Object.values(COIN_IDS).join(",");
|
||||
|
||||
/**
|
||||
* Configuration accepted by {@link CoinGeckoProvider}. Extends the shared
|
||||
* `NoKeyConfig` (optional `baseUrl` + injectable `fetch`) and adds the
|
||||
* optional Pro-tier `apiKey`. Both the free public tier and the Pro tier
|
||||
* use the same endpoint shape, so the key is genuinely optional rather
|
||||
* than mutually exclusive — omitting it issues a keyless request,
|
||||
* supplying it attaches the `x-cg-pro-api-key` header.
|
||||
*/
|
||||
export interface CoinGeckoConfig extends NoKeyConfig {
|
||||
/**
|
||||
* Optional CoinGecko Pro API key. When supplied, every outbound
|
||||
* request carries `x-cg-pro-api-key: <key>`. Absent → free public
|
||||
* tier (10–30 calls/min, no key).
|
||||
*/
|
||||
readonly apiKey?: string;
|
||||
}
|
||||
|
||||
type CoinGeckoResponseShape = Record<string, Record<string, number>>;
|
||||
|
||||
function isResponseShape(value: unknown): value is CoinGeckoResponseShape {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
for (const inner of Object.values(value)) {
|
||||
if (
|
||||
typeof inner !== "object" ||
|
||||
inner === null ||
|
||||
Array.isArray(inner)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function assertTargetsKnown(targets: readonly string[] | undefined): void {
|
||||
if (!targets) return;
|
||||
for (const target of targets) {
|
||||
if (getCoinGeckoId(target) === undefined) {
|
||||
throw new CurrencyProviderError(
|
||||
CURRENCY_PROVIDER_CODES.UNKNOWN_CURRENCY,
|
||||
`${PROVIDER_NAME}: no CoinGecko id for target symbol "${target}"`,
|
||||
{ providerName: PROVIDER_NAME }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function parseJsonBody(response: Response): Promise<unknown> {
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (cause) {
|
||||
throw new CurrencyProviderError(
|
||||
CURRENCY_PROVIDER_CODES.PARSE_ERROR,
|
||||
`${PROVIDER_NAME}: failed to parse response body as JSON`,
|
||||
{
|
||||
providerName: PROVIDER_NAME,
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function buildRates(
|
||||
data: CoinGeckoResponseShape,
|
||||
vsCurrency: string,
|
||||
targets: readonly string[] | undefined
|
||||
): Record<string, number> {
|
||||
const idToSymbol = new Map<string, string>();
|
||||
for (const [symbol, id] of Object.entries(COIN_IDS)) {
|
||||
idToSymbol.set(id, symbol);
|
||||
}
|
||||
|
||||
const allRates: Record<string, number> = {};
|
||||
for (const [id, prices] of Object.entries(data)) {
|
||||
const symbol = idToSymbol.get(id);
|
||||
const price = prices[vsCurrency];
|
||||
if (symbol !== undefined && typeof price === "number" && price > 0) {
|
||||
allRates[symbol] = 1 / price;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targets) return allRates;
|
||||
const filtered: Record<string, number> = {};
|
||||
for (const target of targets) {
|
||||
const value = allRates[target];
|
||||
if (value !== undefined) {
|
||||
filtered[target] = value;
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* CoinGecko crypto-rate provider. Implements the `CurrencyProvider`
|
||||
* contract from `@suaveplan/currency` against the public `/simple/price`
|
||||
* endpoint, routes every request through the shared `httpRequest`
|
||||
* helper from `@suaveplan/currency-providers`, and raises typed
|
||||
* `CurrencyProviderError` on every failure path.
|
||||
*
|
||||
* Both free and Pro tiers share the same URL shape; passing `apiKey`
|
||||
* attaches the `x-cg-pro-api-key` header, omitting it issues a keyless
|
||||
* public-tier request.
|
||||
*/
|
||||
export class CoinGeckoProvider implements CurrencyProvider {
|
||||
public readonly name = PROVIDER_NAME;
|
||||
public readonly supportsCrypto = true;
|
||||
public readonly supportsFiat = true;
|
||||
|
||||
private readonly baseUrl: string;
|
||||
private readonly fetchImpl: FetchLike;
|
||||
private readonly headers: Readonly<Record<string, string>> | undefined;
|
||||
|
||||
public constructor(config: CoinGeckoConfig = {}) {
|
||||
this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
||||
this.fetchImpl = config.fetch ?? (globalThis.fetch as FetchLike);
|
||||
this.headers =
|
||||
config.apiKey !== undefined
|
||||
? Object.freeze({ [PRO_API_KEY_HEADER]: config.apiKey })
|
||||
: undefined;
|
||||
}
|
||||
|
||||
public async fetchRates(
|
||||
options?: RateFetchOptions
|
||||
): Promise<RateFetchResult> {
|
||||
const base = options?.base ?? "USD";
|
||||
const vsCurrency = base.toLowerCase();
|
||||
|
||||
assertTargetsKnown(options?.targets);
|
||||
|
||||
const url = `${this.baseUrl}/simple/price?ids=${ALL_IDS}&vs_currencies=${vsCurrency}`;
|
||||
const init: RequestInit | undefined =
|
||||
this.headers !== undefined
|
||||
? { headers: { ...this.headers } }
|
||||
: undefined;
|
||||
|
||||
const response = await httpRequest({
|
||||
url,
|
||||
fetch: this.fetchImpl,
|
||||
providerName: PROVIDER_NAME,
|
||||
...(init !== undefined ? { init } : {}),
|
||||
...(options?.signal !== undefined
|
||||
? { signal: options.signal }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const body = await parseJsonBody(response);
|
||||
if (!isResponseShape(body)) {
|
||||
throw new CurrencyProviderError(
|
||||
CURRENCY_PROVIDER_CODES.UPSTREAM_ERROR,
|
||||
`${PROVIDER_NAME}: response body did not match Record<string, Record<string, number>>`,
|
||||
{ providerName: PROVIDER_NAME }
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
base,
|
||||
rates: buildRates(body, vsCurrency, options?.targets),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type { CoinGeckoConfig } from "./coingecko.js";
|
||||
export { CoinGeckoProvider } from "./coingecko.js";
|
||||
@@ -0,0 +1,5 @@
|
||||
export { COIN_IDS, getCoinGeckoId } from "./coin-ids/coin-ids.js";
|
||||
export {
|
||||
type CoinGeckoConfig,
|
||||
CoinGeckoProvider,
|
||||
} from "./coingecko/coingecko.js";
|
||||
@@ -0,0 +1,7 @@
|
||||
// Polyfills for test environment — none required. The package is universal
|
||||
// and uses only `URLSearchParams`, `Response`, `Date.now`, `Map`, `Object.entries`,
|
||||
// and the injected `FetchLike` (default `globalThis.fetch`) — all available in
|
||||
// every supported runtime (Node 18+, Bun, browsers, Web Workers). This file
|
||||
// exists to give vitest a stable setup file path mirroring the sibling-package
|
||||
// layout.
|
||||
export { };
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "./dist",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "tests/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts", "dist", "node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
cacheDir: "../../../node_modules/.vite/packages/currency-provider-coingecko",
|
||||
plugins: [
|
||||
dts({
|
||||
entryRoot: "src",
|
||||
tsconfigPath: "./tsconfig.lib.json",
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
outDir: "./dist",
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
sourcemap: true,
|
||||
lib: { entry: "src/index.ts", formats: ["es"] },
|
||||
rollupOptions: {
|
||||
treeshake: true,
|
||||
output: {
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: "src",
|
||||
entryFileNames: "[name].js",
|
||||
},
|
||||
external: [
|
||||
"@suaveplan/currency",
|
||||
"@suaveplan/currency-providers",
|
||||
"@suaveplan/error",
|
||||
"@suaveplan/types",
|
||||
"tslib",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: false,
|
||||
environment: "node",
|
||||
pool: "forks",
|
||||
setupFiles: ["./tests/setup/polyfills.ts"],
|
||||
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
|
||||
exclude: ["dist/**", "node_modules/**"],
|
||||
coverage: {
|
||||
provider: "istanbul",
|
||||
reporter: ["text", "html", "lcov"],
|
||||
include: ["src/**/*.ts"],
|
||||
exclude: [
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.types.ts",
|
||||
"src/**/index.ts",
|
||||
"src/**/*.bench.ts",
|
||||
"src/__benchmarks__/**",
|
||||
"tests/**",
|
||||
],
|
||||
thresholds: { statements: 100, branches: 100, functions: 100, lines: 100 },
|
||||
},
|
||||
testTimeout: 30000,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user