diff --git a/.github/workflows/tests-and-lint.yml b/.github/workflows/tests-and-lint.yml new file mode 100644 index 0000000..e9957a7 --- /dev/null +++ b/.github/workflows/tests-and-lint.yml @@ -0,0 +1,35 @@ +name: Tests and Lint + +on: + push: + branches: ["main"] + pull_request: + +permissions: + contents: read + +jobs: + quality: + name: Tests and lint + runs-on: ubuntu-latest + + steps: + - name: Checkout JS client + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run lint + run: yarn lint + + - name: Run tests + run: yarn test:ci diff --git a/README.md b/README.md index 262f261..907b916 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,27 @@ const [subset, metadata] = await client.selectDataset({ }); ``` +Use `bounds` for rectangular lon/lat selections. Tuple bounds are +`[west, south, east, north]`. + +```typescript +const [westernEurope, metadata] = await client.selectDataset({ + request: { + organization: "ecmwf", + collection: "era5", + dataset: "temperature_2m", + variant: "finalized" + }, + selection: { + bounds: [-12, 35, 16, 60], + timeRange: { + start: "2024-01-01T00:00:00Z", + end: "2024-01-07T23:00:00Z", + }, + } +}); +``` + ### Geographic shape selections The client supports advanced geographic selections beyond single points: diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..90137a9 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,24 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["coverage/**", "dist/**", "node_modules/**"], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.ts", "**/*.js"], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + }, + rules: { + "@typescript-eslint/no-this-alias": "off", + "no-undef": "off", + }, + }, +); diff --git a/package.json b/package.json index 7ec9c75..957afde 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dclimate/dclimate-client-js", - "version": "0.5.7", + "version": "0.5.8", "description": "JavaScript client for dClimate datasets using jaxray and IPFS stores", "type": "module", "main": "./dist/node/index.js", @@ -26,7 +26,9 @@ "build:browser": "tsc -p tsconfig.browser.json", "build:node": "tsc -p tsconfig.node.json", "build": "npm run build:browser && npm run build:node", + "lint": "eslint .", "test": "vitest", + "test:ci": "vitest run", "test:coverage": "vitest run --coverage", "prepare": "npm run build", "prepublishOnly": "npm run build" @@ -53,7 +55,11 @@ "@opentelemetry/api": "^1.9.1" }, "devDependencies": { + "@eslint/js": "10.0.1", + "eslint": "10.4.1", + "globals": "17.6.0", "typescript": "^5.4.0", + "typescript-eslint": "8.61.0", "vitest": "^4.1.0" }, "publishConfig": { diff --git a/src/client.ts b/src/client.ts index df2612c..7635dbf 100644 --- a/src/client.ts +++ b/src/client.ts @@ -161,8 +161,8 @@ export class DClimateClient { request.variant, resolvedOrganization ); - resolvedCollection = resolvedInfo.collectionId; - resolvedOrganization = resolvedInfo.organizationId ?? resolvedOrganization; + const resolvedOrganizationId = + resolvedInfo.organizationId ?? resolvedOrganization; // Multiple variants with concat metadata found // Load and concatenate based on dclimate:concatPriority @@ -170,7 +170,7 @@ export class DClimateClient { { ...request, collection: resolvedInfo.collectionId, - organization: resolvedInfo.organizationId ?? resolvedOrganization, + organization: resolvedOrganizationId, variant: request.variant, }, concatenableItems, @@ -184,7 +184,6 @@ export class DClimateClient { // Fall back to single variant loading let cid: string | null = null; - let resolvedPath: string; let metadataDataset = request.dataset; let metadataCollection = resolvedCollection || request.collection; let metadataVariant = request.variant ?? ""; @@ -231,7 +230,7 @@ export class DClimateClient { // Build path from resolved names const pathParts = [metadataCollection, metadataDataset, metadataVariant].filter(Boolean); - resolvedPath = pathParts.join("-"); + const resolvedPath = pathParts.join("-"); const dataset = await openDatasetFromCid(cid, { gatewayUrl, diff --git a/src/geotemporal-dataset.ts b/src/geotemporal-dataset.ts index 82a05c1..499594f 100644 --- a/src/geotemporal-dataset.ts +++ b/src/geotemporal-dataset.ts @@ -1,16 +1,25 @@ -import { Dataset, DataArray } from "@dclimate/jaxray"; import { - InvalidSelectionError, - NoDataFoundError, -} from "./errors.js"; + Dataset, + DataArray, + type CoordinateValue, + type Selection, +} from "@dclimate/jaxray"; +import { InvalidSelectionError, NoDataFoundError } from "./errors.js"; import { + BoundsSelection, + BoundsSelectionOptions, DatasetMetadata, + DatasetObject, GeoSelectionOptions, PointQueryOptions, TimeRange, } from "./types.js"; import { normalizeTimeRange, normalizeSegment } from "./utils.js"; -import { points as pointsShape, circle as circleShape, rectangle as rectangleShape } from "./shapes/index.js"; +import { + points as pointsShape, + circle as circleShape, + rectangle as rectangleShape, +} from "./shapes/index.js"; type SelectionMethod = Parameters[1] extends infer Options ? Options extends { method?: infer Method } @@ -37,7 +46,7 @@ const DEFAULT_TIME_KEYS = [ export class GeoTemporalDataset { constructor( private readonly dataset: Dataset, - private readonly metadata: DatasetMetadata + private readonly metadata: DatasetMetadata, ) {} get info(): DatasetMetadata { @@ -56,8 +65,8 @@ export class GeoTemporalDataset { return this.dataset.coords; } - toObject(): any { - return this.dataset.toObject(); + toObject(): DatasetObject { + return this.dataset.toObject() as DatasetObject; } toJSON(): string { @@ -66,7 +75,7 @@ export class GeoTemporalDataset { async toRecords( varName: string, - options?: { precision?: number } + options?: { precision?: number }, ): Promise>> { const dataArray = this.dataset.getVariable(varName); if (!dataArray) { @@ -95,6 +104,12 @@ export class GeoTemporalDataset { async select(options: GeoSelectionOptions): Promise { let current: GeoTemporalDataset = this; + if (options.point && options.bounds) { + throw new InvalidSelectionError( + "Use either point or bounds selection, not both.", + ); + } + // Apply selections in order: point first, then time range // Point selection must come first because it changes the dataset structure if (options.point) { @@ -116,13 +131,26 @@ export class GeoTemporalDataset { } } + if (options.bounds) { + const { west, south, east, north, boundsOptions } = + normalizeBoundsSelection(options.bounds, options.boundsOptions); + const subset = await current.rectangle( + south, + west, + north, + east, + boundsOptions, + ); + current = current.wrapDataset(subset); + } + return current; } async point( latitude: number, longitude: number, - options: PointQueryOptions = {} + options: PointQueryOptions = {}, ): Promise { const latKey = options.latitudeKey ?? this.inferCoordinateKey(DEFAULT_LATITUDE_KEYS); @@ -131,7 +159,7 @@ export class GeoTemporalDataset { if (!latKey || !lonKey) { throw new InvalidSelectionError( - "Latitude/longitude coordinates were not found in the dataset." + "Latitude/longitude coordinates were not found in the dataset.", ); } @@ -149,50 +177,51 @@ export class GeoTemporalDataset { async timeRange( range: TimeRange, - dimension = "time" + dimension = "time", ): Promise { const candidateKeys = Array.from( - new Set([dimension, ...DEFAULT_TIME_KEYS]) + new Set([dimension, ...DEFAULT_TIME_KEYS]), ).filter(Boolean) as string[]; const timeKey = this.inferCoordinateKey(candidateKeys); if (!timeKey) { throw new InvalidSelectionError( - `Coordinate "${dimension}" not found in dataset.` + `Coordinate "${dimension}" not found in dataset.`, ); } const coords = this.dataset.coords[timeKey]; if (!Array.isArray(coords) || coords.length === 0) { throw new InvalidSelectionError( - `Coordinate "${timeKey}" not found in dataset.` + `Coordinate "${timeKey}" not found in dataset.`, ); } - let normalizedRange: { start: unknown; end: unknown }; + let normalizedRange: { start: CoordinateValue; end: CoordinateValue }; try { normalizedRange = normalizeTimeRange(range, coords); } catch (error) { throw new InvalidSelectionError( `Unable to normalize time range: ${String( - (error as Error).message ?? error - )}` + (error as Error).message ?? error, + )}`, ); } let subset: Dataset; try { - subset = await this.dataset.sel({ + const selection: Selection = { [timeKey]: { - start: normalizedRange.start as any, - stop: normalizedRange.end as any, + start: normalizedRange.start, + stop: normalizedRange.end, }, - }); + }; + subset = await this.dataset.sel(selection); } catch (error) { throw new InvalidSelectionError( `Failed to apply time range on "${timeKey}": ${String( - (error as Error).message ?? error - )}` + (error as Error).message ?? error, + )}`, ); } @@ -204,7 +233,7 @@ export class GeoTemporalDataset { private inferCoordinateKey(candidates: string[]): string | undefined { const coords = this.dataset.coords; const normalizedKeys = Object.keys(coords).map((key) => - normalizeSegment(key) + normalizeSegment(key), ); for (const candidate of candidates) { @@ -218,9 +247,10 @@ export class GeoTemporalDataset { return undefined; } - private buildSelectionOptions( - options: PointQueryOptions - ): { method?: SelectionMethod; tolerance?: number } { + private buildSelectionOptions(options: PointQueryOptions): { + method?: SelectionMethod; + tolerance?: number; + } { const method = options.method === "exact" ? undefined : ("nearest" as SelectionMethod); const selectionOptions: { method?: SelectionMethod; tolerance?: number } = @@ -235,6 +265,11 @@ export class GeoTemporalDataset { return selectionOptions; } + private wrapDataset(dataset: Dataset): GeoTemporalDataset { + const wrapped = new GeoTemporalDataset(dataset, this.metadata); + wrapped.ensureHasData(); + return wrapped; + } /** * Select data at specific point coordinates @@ -253,7 +288,7 @@ export class GeoTemporalDataset { tolerance?: number; latitudeKey?: string; longitudeKey?: string; - } + }, ): Promise { return await pointsShape(this.dataset, pointLats, pointLons, options); } @@ -274,9 +309,15 @@ export class GeoTemporalDataset { options?: { latitudeKey?: string; longitudeKey?: string; - } + }, ): Promise { - return await circleShape(this.dataset, centerLat, centerLon, radiusKm, options); + return await circleShape( + this.dataset, + centerLat, + centerLon, + radiusKm, + options, + ); } /** @@ -297,8 +338,78 @@ export class GeoTemporalDataset { options?: { latitudeKey?: string; longitudeKey?: string; - } + }, ): Promise { - return await rectangleShape(this.dataset, minLat, minLon, maxLat, maxLon, options); + return await rectangleShape( + this.dataset, + minLat, + minLon, + maxLat, + maxLon, + options, + ); + } +} + +function normalizeBoundsSelection( + bounds: BoundsSelection, + fallbackOptions?: BoundsSelectionOptions, +): { + west: number; + south: number; + east: number; + north: number; + boundsOptions?: BoundsSelectionOptions; +} { + const isTuple = isBoundsSelectionTuple(bounds); + const normalized = isTuple + ? { + west: bounds[0], + south: bounds[1], + east: bounds[2], + north: bounds[3], + boundsOptions: fallbackOptions, + } + : { + west: bounds.west, + south: bounds.south, + east: bounds.east, + north: bounds.north, + boundsOptions: bounds.options ?? fallbackOptions, + }; + + if ( + [normalized.west, normalized.south, normalized.east, normalized.north].some( + (value) => typeof value !== "number" || !Number.isFinite(value), + ) + ) { + throw new InvalidSelectionError( + "Bounds selection must use finite west, south, east, and north numbers.", + ); } + + if (normalized.west >= normalized.east) { + throw new InvalidSelectionError( + `west (${normalized.west}) must be less than east (${normalized.east}).`, + ); + } + + if (normalized.south >= normalized.north) { + throw new InvalidSelectionError( + `south (${normalized.south}) must be less than north (${normalized.north}).`, + ); + } + + return normalized; +} + +function isBoundsSelectionTuple( + bounds: BoundsSelection, +): bounds is readonly [ + west: number, + south: number, + east: number, + north: number, +] { + return Array.isArray(bounds); } diff --git a/src/shapes/circle.ts b/src/shapes/circle.ts index 74b0c75..7e01cdc 100644 --- a/src/shapes/circle.ts +++ b/src/shapes/circle.ts @@ -175,10 +175,10 @@ export async function circle( // Use isel to select only the valid indices let filtered = await masked.isel({ [latitudeKey]: validLatIndices, - } as any); + }); filtered = await filtered.isel({ [longitudeKey]: validLonIndices, - } as any); + }); return filtered; } diff --git a/src/shapes/points.ts b/src/shapes/points.ts index eb9b1ad..50aad00 100644 --- a/src/shapes/points.ts +++ b/src/shapes/points.ts @@ -1,4 +1,4 @@ -import { Dataset, DataArray } from "@dclimate/jaxray"; +import { Dataset, DataArray, type Selection } from "@dclimate/jaxray"; import { InvalidSelectionError, NoDataFoundError } from "../errors.js"; /** @@ -76,16 +76,18 @@ export async function points( try { if (snapToGrid) { - selectedData = await dataset.sel({ + const selection = { [latitudeKey]: lats, [longitudeKey]: lons, - } as any); + } as unknown as Selection; + selectedData = await dataset.sel(selection); } else { + const selection = { + [latitudeKey]: lats, + [longitudeKey]: lons, + } as unknown as Selection; selectedData = await dataset.sel( - { - [latitudeKey]: lats, - [longitudeKey]: lons, - } as any, + selection, { method: "nearest", tolerance, diff --git a/src/shapes/rectangle.ts b/src/shapes/rectangle.ts index b5df557..550fb56 100644 --- a/src/shapes/rectangle.ts +++ b/src/shapes/rectangle.ts @@ -118,7 +118,7 @@ export async function rectangle( const filtered = await dataset.sel({ [latitudeKey]: selectedLats, [longitudeKey]: selectedLons, - } as any); + }); return filtered; } diff --git a/src/stac/stac-catalog.ts b/src/stac/stac-catalog.ts index 717c1f4..396449d 100644 --- a/src/stac/stac-catalog.ts +++ b/src/stac/stac-catalog.ts @@ -77,7 +77,7 @@ export interface StacLink { type?: string; title?: string; // For dclimate:id, dclimate:types and other arbitrary metadata - [key: string]: any; + [key: string]: unknown; } export interface StacAsset { @@ -91,8 +91,8 @@ export interface StacItem { type: "Feature"; stac_version: string; id: string; - properties: Record; - geometry: any; + properties: Record; + geometry: unknown; bbox?: number[]; assets: Record; links: StacLink[]; @@ -106,8 +106,8 @@ export interface StacCollection { description?: string; keywords?: string[]; license?: string; - extent?: any; - summaries?: Record; + extent?: unknown; + summaries?: Record; links: StacLink[]; items?: StacItem[]; // Loaded items organizationId?: string; @@ -133,6 +133,22 @@ interface CatalogCacheEntry { rootCid: string; } +export function getStringProperty( + properties: Record | undefined, + key: string +): string | undefined { + const value = properties?.[key]; + return typeof value === "string" ? value : undefined; +} + +function getNumberProperty( + properties: Record | undefined, + key: string +): number | undefined { + const value = properties?.[key]; + return typeof value === "number" ? value : undefined; +} + export interface StacCatalogOptions { gatewayUrl?: string; cacheTtlMs?: number; // Default: 3600000 (1 hour) @@ -629,20 +645,20 @@ export function getConcatenableItemsFromStac( // Check if this item matches our dataset if (itemCollection === collection && itemDataset === dataset) { // Check for concatenation metadata in properties - const concatPriority = item.properties["dclimate:concatPriority"]; - const concatDimension = item.properties["dclimate:concatDimension"]; - // Also check in link metadata (fallback) const itemLink = collectionObj.links.find( (link) => link.rel === "item" && link?.["dclimate:id"] === item.id ); - const linkConcatPriority = itemLink?.["dclimate:concatPriority"]; - const linkConcatDimension = itemLink?.["dclimate:concatDimension"]; - - const priority = concatPriority ?? linkConcatPriority; - const dimension = concatDimension ?? linkConcatDimension ?? "time"; + const priority = + getNumberProperty(item.properties, "dclimate:concatPriority") ?? + getNumberProperty(itemLink, "dclimate:concatPriority") ?? + 0; + const dimension = + getStringProperty(item.properties, "dclimate:concatDimension") ?? + getStringProperty(itemLink, "dclimate:concatDimension") ?? + "time"; // Extract CID from assets const dataAsset = item.assets.data; @@ -653,7 +669,7 @@ export function getConcatenableItemsFromStac( matchingItems.push({ variant: itemVariant, cid, - concatPriority: priority ?? 0, + concatPriority: priority, concatDimension: dimension, }); } @@ -677,7 +693,7 @@ export function listAvailableDatasetsFromStac( const datasetNamesFromLink = collection.datasetNames || []; // Group items by dataset - const datasetMap = new Map(); + const datasetMap = new Map(); for (const item of collection.items || []) { const parts = item.id.split("-"); @@ -711,8 +727,14 @@ export function listAvailableDatasetsFromStac( }; } - const startDt = item.properties?.start_datetime ?? item.properties?.datetime ?? null; - const endDt = item.properties?.end_datetime ?? item.properties?.datetime ?? null; + const startDt = + getStringProperty(item.properties, "start_datetime") ?? + getStringProperty(item.properties, "datetime") ?? + null; + const endDt = + getStringProperty(item.properties, "end_datetime") ?? + getStringProperty(item.properties, "datetime") ?? + null; if (startDt != null || endDt != null) { variantEntry.temporalExtent = { start: startDt ?? null, diff --git a/src/stac/stac-server.ts b/src/stac/stac-server.ts index afc343f..4798cb3 100644 --- a/src/stac/stac-server.ts +++ b/src/stac/stac-server.ts @@ -11,6 +11,7 @@ import type { DatasetCatalog, DatasetVariantConfig, } from "./stac-catalog.js"; +import { getStringProperty } from "./stac-catalog.js"; export const DEFAULT_STAC_SERVER_URL = "https://api.stac.dclimate.net"; @@ -25,7 +26,7 @@ export interface StacServerItem { type: "Feature"; id: string; collection?: string; - properties: Record; + properties: Record; assets: Record; } @@ -88,7 +89,7 @@ export async function resolveCidFromStacServer( if (variant) { selectedItem = matches.find( - (f) => f.properties["dclimate:variant"] === variant + (f) => getStringProperty(f.properties, "dclimate:variant") === variant ); if (!selectedItem) { throw new Error( @@ -99,12 +100,13 @@ export async function resolveCidFromStacServer( } else { // Prefer: default > final > finalized > latest > first match selectedItem = matches[0]; - resolvedVariant = matches[0].properties["dclimate:variant"] || "default"; + resolvedVariant = + getStringProperty(matches[0].properties, "dclimate:variant") ?? "default"; const preferredOrder = ["default", "final", "finalized", "latest"]; for (const preferred of preferredOrder) { const found = matches.find( - (f) => f.properties["dclimate:variant"] === preferred + (f) => getStringProperty(f.properties, "dclimate:variant") === preferred ); if (found) { selectedItem = found; @@ -160,7 +162,7 @@ interface StacServerSearchFeature { id: string; collection?: string; bbox?: number[]; - properties: Record; + properties: Record; } interface StacServerSearchPage { @@ -269,14 +271,16 @@ export async function listAvailableDatasetsFromStacServer( // that pre-date the dclimate:* property convention. const idParts = feature.id.split("-"); const datasetName = - (props["dclimate:dataset_id"] as string | undefined) ?? + getStringProperty(props, "dclimate:dataset_id") ?? (idParts.length >= 2 ? idParts[1] : undefined); const variantName = - (props["dclimate:variant"] as string | undefined) ?? + getStringProperty(props, "dclimate:variant") ?? (idParts.length >= 3 ? idParts.slice(2).join("-") : "default"); if (!datasetName) continue; - const cid = stripIpfsScheme(props["dclimate:latest_dataset_cid"] as string | undefined); + const cid = stripIpfsScheme( + getStringProperty(props, "dclimate:latest_dataset_cid") + ); const variantConfig: DatasetVariantConfig = { variant: variantName }; if (cid) variantConfig.cid = cid; @@ -288,8 +292,14 @@ export async function listAvailableDatasetsFromStacServer( }; } - const startDt = props.start_datetime ?? props.datetime ?? null; - const endDt = props.end_datetime ?? props.datetime ?? null; + const startDt = + getStringProperty(props, "start_datetime") ?? + getStringProperty(props, "datetime") ?? + null; + const endDt = + getStringProperty(props, "end_datetime") ?? + getStringProperty(props, "datetime") ?? + null; if (startDt !== null || endDt !== null) { variantConfig.temporalExtent = { start: startDt, end: endDt }; } diff --git a/src/types.ts b/src/types.ts index 313454c..78f27c4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,17 +27,34 @@ export interface PointQueryOptions { tolerance?: number; } +export interface BoundsSelectionOptions { + latitudeKey?: string; + longitudeKey?: string; +} + export interface TimeRange { start: Date | string; end: Date | string; } +export type BoundsSelection = + | readonly [west: number, south: number, east: number, north: number] + | { + west: number; + south: number; + east: number; + north: number; + options?: BoundsSelectionOptions; + }; + export interface GeoSelectionOptions { point?: { latitude: number; longitude: number; options?: PointQueryOptions; }; + bounds?: BoundsSelection; + boundsOptions?: BoundsSelectionOptions; timeRange?: TimeRange; } @@ -70,3 +87,20 @@ export interface DatasetRequest { organization?: string; cid?: string; } + +export interface DataArrayObject { + data: unknown; + dims: string[]; + coords: Record; + attrs: Record; + name?: string; + shape: number[]; +} + +export interface DatasetObject { + dataVars: Record; + coords: Record; + attrs: Record; + dims: string[]; + sizes: Record; +} diff --git a/src/utils.ts b/src/utils.ts index 83e6f2d..20391ec 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,7 +21,7 @@ export function isDatasetEmpty(dataset: Dataset): boolean { return Object.values(sizes).some((size) => size === 0); } -function toDate(value: Date | string): Date | null { +function toDate(value: Date | string | number): Date | null { if (value instanceof Date) return value; const parsed = new Date(value); return Number.isNaN(parsed.getTime()) ? null : parsed; @@ -71,8 +71,8 @@ export function normalizeTimeRange( return { start: startValue, end: endValue }; } - const startDate = toDate(startValue as any); - const endDate = toDate(endValue as any); + const startDate = toDate(startValue); + const endDate = toDate(endValue); if (startDate && endDate && startDate > endDate) { return { start: endDate.toISOString(), diff --git a/tests/geotemporal-dataset.test.ts b/tests/geotemporal-dataset.test.ts index 0806e07..a17d8c7 100644 --- a/tests/geotemporal-dataset.test.ts +++ b/tests/geotemporal-dataset.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from "vitest"; -import { DatasetMetadata, DClimateClient, GeoTemporalDataset } from "../src/index.js"; -import { InvalidSelectionError, NoDataFoundError } from "../src/errors.js"; +import { DatasetMetadata, DatasetRequest, DClimateClient, GeoTemporalDataset } from "../src/index.js"; +import { InvalidSelectionError } from "../src/errors.js"; describe("GeoTemporalDataset - Real Data Integration Tests", () => { const client = new DClimateClient(); -const DATASET_REQUESTS: Record = { +const DATASET_REQUESTS: Record = { fpar: { collection: "copernicus_clms", organization: "copernicus", dataset: "fpar", variant: "default" }, "ifs-temperature": { collection: "ifs", organization: "ecmwf", dataset: "temperature_forecast", variant: "default" }, "ifs-precip": { collection: "ifs", organization: "ecmwf", dataset: "precipitation_forecast", variant: "default" }, diff --git a/tests/haversine.test.ts b/tests/haversine.test.ts index 59350b3..573034c 100644 --- a/tests/haversine.test.ts +++ b/tests/haversine.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest"; import { haversine } from "../src/math/haversine"; describe("haversine", () => { - it.only("should calculate distance between two points (scalar inputs)", () => { + it("should calculate distance between two points (scalar inputs)", () => { // New York City to Los Angeles const lat1 = 40.7128; const lon1 = -74.006; @@ -11,7 +11,7 @@ describe("haversine", () => { const distance = haversine(lat1, lon1, lat2, lon2); - // Expected distance is approximately 3944 km + // Expected distance is approximately 3936 km with a 6371 km earth radius expect(typeof distance).toBe("number"); expect(distance).toBeCloseTo(3936, -1); // Within 10 km }); @@ -61,8 +61,8 @@ describe("haversine", () => { expect(Array.isArray(distances)).toBe(true); expect(distances).toHaveLength(2); // Both should be the same distance (NYC to LA and back) - expect((distances as number[])[0]).toBeCloseTo(3944, -1); - expect((distances as number[])[1]).toBeCloseTo(3944, -1); + expect((distances as number[])[0]).toBeCloseTo(3936, -1); + expect((distances as number[])[1]).toBeCloseTo(3936, -1); }); it("should handle mixed scalar and array inputs", () => { @@ -99,8 +99,8 @@ describe("haversine", () => { const distance = haversine(lat1, lon1, lat2, lon2); - // Expected distance is approximately 6930 km - expect(distance).toBeCloseTo(6930, -1); + // Expected distance is approximately 6870 km with a 6371 km earth radius + expect(distance).toBeCloseTo(6870, -1); }); it("should broadcast arrays of different lengths", () => { diff --git a/tests/helpers/fake-dataset.ts b/tests/helpers/fake-dataset.ts index b5d5b0e..1698177 100644 --- a/tests/helpers/fake-dataset.ts +++ b/tests/helpers/fake-dataset.ts @@ -1,4 +1,20 @@ type CoordinateKey = "latitude" | "longitude" | "time"; +type FakeSelectionRange = { + start: string | number | Date; + stop: string | number | Date; +}; +type FakeSelectionValue = + | number + | Array + | FakeSelectionRange; +type FakeVariableRecords = Array< + Record +>; +type FakeDataVariable = { + compute: () => Promise<{ + toRecords: () => FakeVariableRecords; + }>; +}; export interface SampleRecord { latitude: number; @@ -7,14 +23,14 @@ export interface SampleRecord { value: number; } -const COORDINATE_KEYS: CoordinateKey[] = ["latitude", "longitude", "time"]; - function uniqueValues(values: T[]): T[] { return Array.from(new Set(values)); } function sortTimes(times: string[]): string[] { - return [...times].sort((a, b) => new Date(a).getTime() - new Date(b).getTime()); + return [...times].sort( + (a, b) => new Date(a).getTime() - new Date(b).getTime(), + ); } function normalizeRecords(records: SampleRecord[]): SampleRecord[] { @@ -24,7 +40,20 @@ function normalizeRecords(records: SampleRecord[]): SampleRecord[] { })); } -function pickNearest(records: SampleRecord[], key: "latitude" | "longitude", target: number): number { +function isFakeSelectionRange(value: unknown): value is FakeSelectionRange { + return ( + typeof value === "object" && + value !== null && + "start" in value && + "stop" in value + ); +} + +function pickNearest( + records: SampleRecord[], + key: "latitude" | "longitude", + target: number, +): number { let best = records[0][key]; let bestDistance = Math.abs(best - target); for (const record of records) { @@ -46,9 +75,15 @@ export class FakeDataset { constructor(records: SampleRecord[]) { this.internalRecords = normalizeRecords(records); this.coordMap = { - latitude: uniqueValues(this.internalRecords.map((record) => record.latitude)), - longitude: uniqueValues(this.internalRecords.map((record) => record.longitude)), - time: sortTimes(uniqueValues(this.internalRecords.map((record) => record.time))), + latitude: uniqueValues( + this.internalRecords.map((record) => record.latitude), + ), + longitude: uniqueValues( + this.internalRecords.map((record) => record.longitude), + ), + time: sortTimes( + uniqueValues(this.internalRecords.map((record) => record.time)), + ), }; this.sizeMap = { latitude: this.coordMap.latitude.length, @@ -69,21 +104,38 @@ export class FakeDataset { return this.variables; } - async sel(selection: Record): Promise { + async sel( + selection: Partial>, + ): Promise { let filtered = this.internalRecords; - if (typeof selection.latitude === "number") { - const nearestLat = pickNearest(filtered, "latitude", selection.latitude); + const latitudeSelection = selection.latitude; + if (typeof latitudeSelection === "number") { + const nearestLat = pickNearest(filtered, "latitude", latitudeSelection); filtered = filtered.filter((record) => record.latitude === nearestLat); + } else if (Array.isArray(latitudeSelection)) { + filtered = filtered.filter((record) => + latitudeSelection.includes(record.latitude), + ); } - if (typeof selection.longitude === "number") { - const nearestLon = pickNearest(filtered, "longitude", selection.longitude); + const longitudeSelection = selection.longitude; + if (typeof longitudeSelection === "number") { + const nearestLon = pickNearest( + filtered, + "longitude", + longitudeSelection, + ); filtered = filtered.filter((record) => record.longitude === nearestLon); + } else if (Array.isArray(longitudeSelection)) { + filtered = filtered.filter((record) => + longitudeSelection.includes(record.longitude), + ); } - if (selection.time && typeof selection.time === "object") { - const { start, stop } = selection.time; + const timeSelection = selection.time; + if (isFakeSelectionRange(timeSelection)) { + const { start, stop } = timeSelection; const startDate = new Date(start); const stopDate = new Date(stop); filtered = filtered.filter((record) => { @@ -99,26 +151,27 @@ export class FakeDataset { return new FakeDataset(filtered); } - getVariable(name: string): any { + getVariable(name: string): FakeDataVariable | null { if (name !== "precipitation") { return null; } // Return an object that mimics a DataArray with compute() and toRecords() return { compute: async () => ({ - toRecords: () => this.internalRecords.map((record) => ({ - latitude: record.latitude, - longitude: record.longitude, - time: record.time, - value: record.value, - })) - }) + toRecords: () => + this.internalRecords.map((record) => ({ + latitude: record.latitude, + longitude: record.longitude, + time: record.time, + value: record.value, + })), + }), }; } async toRecords( - variableName: string - ): Promise>> { + variableName: string, + ): Promise { if (variableName !== "precipitation") { return []; } @@ -158,6 +211,8 @@ export const SAMPLE_RECORDS: SampleRecord[] = [ }, ]; -export function createMockDataset(records: SampleRecord[] = SAMPLE_RECORDS): FakeDataset { +export function createMockDataset( + records: SampleRecord[] = SAMPLE_RECORDS, +): FakeDataset { return new FakeDataset(records); } diff --git a/tests/list-datasets-parity.test.ts b/tests/list-datasets-parity.test.ts index af187b0..473d817 100644 --- a/tests/list-datasets-parity.test.ts +++ b/tests/list-datasets-parity.test.ts @@ -90,7 +90,6 @@ const haveBoth = stacCatalog !== null && ipfsCatalog !== null && !ipfsDegraded; if (ipfsDegraded) { - // eslint-disable-next-line no-console console.warn( "[parity] IPFS catalog returned 0 collections while STAC server returned " + `${stacCatalog!.length} — IPFS gateway can serve the root pointer but not ` + diff --git a/tests/metadata.test.ts b/tests/metadata.test.ts index 5bdf05f..ebf8485 100644 --- a/tests/metadata.test.ts +++ b/tests/metadata.test.ts @@ -158,7 +158,7 @@ describe("Dataset metadata", () => { for (const field of requiredFields) { expect(metadata).toHaveProperty(field); - expect((metadata as any)[field]).toBeDefined(); + expect((metadata as unknown as Record)[field]).toBeDefined(); } }, 30000); @@ -174,7 +174,7 @@ describe("Dataset metadata", () => { }) as [GeoTemporalDataset, DatasetMetadata]; // timestamp field was removed in STAC migration - expect((metadata as any).timestamp).toBeUndefined(); + expect((metadata as unknown as Record).timestamp).toBeUndefined(); // fetchedAt is the replacement for timestamp expect(metadata.fetchedAt).toBeDefined(); diff --git a/tests/selection-options.test.ts b/tests/selection-options.test.ts new file mode 100644 index 0000000..d86b657 --- /dev/null +++ b/tests/selection-options.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from "vitest"; +import { GeoTemporalDataset } from "../src/index.js"; +import { InvalidSelectionError } from "../src/errors.js"; +import { createMockDataset } from "./helpers/fake-dataset.js"; +import type { DatasetMetadata } from "../src/index.js"; + +const metadata: DatasetMetadata = { + dataset: "precipitation", + collection: "test", + variant: "default", + organization: "dclimate", + path: "test-precipitation-default", + cid: "bafy-test", + source: "direct_cid", + fetchedAt: new Date("2024-01-01T00:00:00Z"), +}; + +describe("GeoSelectionOptions", () => { + it("supports bounds and time range selections in one call", async () => { + const dataset = new GeoTemporalDataset( + createMockDataset() as never, + metadata, + ); + + const selected = await dataset.select({ + bounds: [-75, 40, -73, 41], + timeRange: { + start: "2023-01-01T00:00:00Z", + end: "2023-01-04T00:00:00Z", + }, + }); + + const records = await selected.toRecords("precipitation"); + + expect(records).toHaveLength(2); + expect(records.map((record) => record.value)).toEqual([10, 12]); + }); + + it("supports object bounds with coordinate key options", async () => { + const dataset = new GeoTemporalDataset( + createMockDataset() as never, + metadata, + ); + + const selected = await dataset.select({ + bounds: { + west: -75, + south: 40, + east: -73, + north: 41, + options: { + latitudeKey: "latitude", + longitudeKey: "longitude", + }, + }, + }); + + const records = await selected.toRecords("precipitation"); + + expect(records).toHaveLength(3); + expect(records.every((record) => record.longitude === -73.99)).toBe(true); + }); + + it("rejects ambiguous point and bounds selections", async () => { + const dataset = new GeoTemporalDataset( + createMockDataset() as never, + metadata, + ); + + await expect( + dataset.select({ + bounds: [-75, 40, -73, 41], + point: { + latitude: 40.75, + longitude: -73.99, + }, + }), + ).rejects.toThrow(InvalidSelectionError); + }); +}); diff --git a/tests/shapes.test.ts b/tests/shapes.test.ts index 60039cd..2107c62 100644 --- a/tests/shapes.test.ts +++ b/tests/shapes.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach } from "vitest"; import { Dataset, DataArray } from "@dclimate/jaxray"; import { points, circle, rectangle } from "../src/shapes/index.js"; -import { InvalidSelectionError, NoDataFoundError } from "../src/errors.js"; +import { InvalidSelectionError } from "../src/errors.js"; import { isDatasetEmpty } from "../src/utils.js"; // Helper to create a test dataset @@ -69,8 +69,12 @@ describe("Shapes Module", () => { const result = await points(dataset, [40.0, 40.5], [-74.0, -73.5]); const tempVar = result.getVariable("temperature"); - expect(Array.isArray(tempVar.data)).toBe(true); - expect((tempVar.data as any[]).length).toBeGreaterThan(0); + const tempData = tempVar.data; + expect(Array.isArray(tempData)).toBe(true); + if (!Array.isArray(tempData)) { + throw new Error("Expected temperature data to be an array"); + } + expect(tempData.length).toBeGreaterThan(0); }); it("should throw error if point arrays have different lengths", async () => { diff --git a/tests/usage.test.ts b/tests/usage.test.ts index 2b070be..7ee7f44 100644 --- a/tests/usage.test.ts +++ b/tests/usage.test.ts @@ -6,7 +6,6 @@ import { import { createMockDataset, SAMPLE_RECORDS } from "./helpers/fake-dataset.js"; const openDatasetFromCidMock = vi.hoisted(() => vi.fn()); -const fetchMock = vi.hoisted(() => vi.fn()); vi.mock("../src/ipfs/open-dataset.js", () => ({ openDatasetFromCid: openDatasetFromCidMock, @@ -40,8 +39,9 @@ describe("DClimateClient usage", () => { expect(metadata.cid).toMatch(/^bafy/); // Real CID from STAC expect(metadata.source).toBe("stac"); // timestamp and url fields removed in STAC migration - expect((metadata as any).timestamp).toBeUndefined(); - expect((metadata as any).url).toBeUndefined(); + const metadataRecord = metadata as unknown as Record; + expect(metadataRecord.timestamp).toBeUndefined(); + expect(metadataRecord.url).toBeUndefined(); const point = await (dataset as GeoTemporalDataset).point(40.75, -73.99); const slice = await point.timeRange({ diff --git a/yarn.lock b/yarn.lock index ad8c75a..2c6e367 100644 --- a/yarn.lock +++ b/yarn.lock @@ -38,6 +38,90 @@ dependencies: tslib "^2.4.0" +"@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" + integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.12.2": + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== + +"@eslint/config-array@^0.23.5": + version "0.23.5" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.23.5.tgz#56e86d243049195d8acc0c06a1b3dfdc3fa3de95" + integrity sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA== + dependencies: + "@eslint/object-schema" "^3.0.5" + debug "^4.3.1" + minimatch "^10.2.4" + +"@eslint/config-helpers@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.6.0.tgz#ef9a36881d39dfd5dbeac22b0da997fabfb08b03" + integrity sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA== + dependencies: + "@eslint/core" "^1.2.1" + +"@eslint/core@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-1.2.1.tgz#c1da7cd1b82fa8787f98b5629fb811848a1b63ce" + integrity sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/js@10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-10.0.1.tgz#1e8a876f50117af8ab67e47d5ad94d38d6622583" + integrity sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA== + +"@eslint/object-schema@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.5.tgz#88e9bf4d11d2b19c082e78ebe7ce88724a5eb091" + integrity sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw== + +"@eslint/plugin-kit@^0.7.2": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz#4b0962f3f2c7ce8bc98b3ecfe34525c09d2cb729" + integrity sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A== + dependencies: + "@eslint/core" "^1.2.1" + levn "^0.4.1" + +"@humanfs/core@^0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.2.tgz#a8272ca03b2acf492670222b2320b6c421bfde60" + integrity sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA== + dependencies: + "@humanfs/types" "^0.15.0" + +"@humanfs/node@^0.16.6": + version "0.16.8" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.8.tgz#8f800cccc13f4f8cd3116e2d9c0a94939da3e3ed" + integrity sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ== + dependencies: + "@humanfs/core" "^0.19.2" + "@humanfs/types" "^0.15.0" + "@humanwhocodes/retry" "^0.4.0" + +"@humanfs/types@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@humanfs/types/-/types-0.15.0.tgz#f2a09f62012390b2bff3fc6fb248ddec8c09a090" + integrity sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q== + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" + integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== + "@ipld/dag-cbor@^9.2.5": version "9.2.5" resolved "https://registry.yarnpkg.com/@ipld/dag-cbor/-/dag-cbor-9.2.5.tgz#4f792380966190e7d5a82a8d9ba9c7a8589eb1b0" @@ -192,11 +276,122 @@ resolved "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz" integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== +"@types/esrecurse@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec" + integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw== + "@types/estree@^1.0.0": version "1.0.8" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== +"@types/estree@^1.0.6", "@types/estree@^1.0.8": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24" + integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@typescript-eslint/eslint-plugin@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz#db20271974b94a3a54d3b9544e5f5b3481448400" + integrity sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw== + dependencies: + "@eslint-community/regexpp" "^4.12.2" + "@typescript-eslint/scope-manager" "8.61.0" + "@typescript-eslint/type-utils" "8.61.0" + "@typescript-eslint/utils" "8.61.0" + "@typescript-eslint/visitor-keys" "8.61.0" + ignore "^7.0.5" + natural-compare "^1.4.0" + ts-api-utils "^2.5.0" + +"@typescript-eslint/parser@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.0.tgz#1afe73c9ccce16b7a26d6b95f9400b0ccc34af87" + integrity sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w== + dependencies: + "@typescript-eslint/scope-manager" "8.61.0" + "@typescript-eslint/types" "8.61.0" + "@typescript-eslint/typescript-estree" "8.61.0" + "@typescript-eslint/visitor-keys" "8.61.0" + debug "^4.4.3" + +"@typescript-eslint/project-service@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.0.tgz#417a2feac32e8ebd336d63f068c3b42b736ea1ac" + integrity sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.61.0" + "@typescript-eslint/types" "^8.61.0" + debug "^4.4.3" + +"@typescript-eslint/scope-manager@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz#93c2520d05653fe65eb9ee98efc74fd0134a7852" + integrity sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA== + dependencies: + "@typescript-eslint/types" "8.61.0" + "@typescript-eslint/visitor-keys" "8.61.0" + +"@typescript-eslint/tsconfig-utils@8.61.0", "@typescript-eslint/tsconfig-utils@^8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz#05d6e3ff20001674ebcd22d03dac29ee448043ba" + integrity sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ== + +"@typescript-eslint/type-utils@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz#50219b57e6b89cecfb1a15f093b15ec9ee019974" + integrity sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A== + dependencies: + "@typescript-eslint/types" "8.61.0" + "@typescript-eslint/typescript-estree" "8.61.0" + "@typescript-eslint/utils" "8.61.0" + debug "^4.4.3" + ts-api-utils "^2.5.0" + +"@typescript-eslint/types@8.61.0", "@typescript-eslint/types@^8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.0.tgz#0ddb46e012a4288292950bdd253db42f278ce64d" + integrity sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg== + +"@typescript-eslint/typescript-estree@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz#98ca47260bbf627fc28f018b3a0abf00e3090690" + integrity sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA== + dependencies: + "@typescript-eslint/project-service" "8.61.0" + "@typescript-eslint/tsconfig-utils" "8.61.0" + "@typescript-eslint/types" "8.61.0" + "@typescript-eslint/visitor-keys" "8.61.0" + debug "^4.4.3" + minimatch "^10.2.2" + semver "^7.7.3" + tinyglobby "^0.2.15" + ts-api-utils "^2.5.0" + +"@typescript-eslint/utils@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.0.tgz#ed3546a052787e84ea6c5064d0919fc5eea8522f" + integrity sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA== + dependencies: + "@eslint-community/eslint-utils" "^4.9.1" + "@typescript-eslint/scope-manager" "8.61.0" + "@typescript-eslint/types" "8.61.0" + "@typescript-eslint/typescript-estree" "8.61.0" + +"@typescript-eslint/visitor-keys@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz#39b4e1ab8936d23bea973d39fd092f9aa21f275e" + integrity sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ== + dependencies: + "@typescript-eslint/types" "8.61.0" + eslint-visitor-keys "^5.0.0" + "@vitest/expect@4.1.0": version "4.1.0" resolved "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz" @@ -265,11 +460,43 @@ reference-spec-reader "^0.2.0" unzipit "1.4.3" +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.16.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== + +ajv@^6.14.0: + version "6.15.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.15.0.tgz#07e982c74626167aa7a2495c53817892d7139492" + integrity sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + assertion-error@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz" integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== +balanced-match@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" + integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== + +brace-expansion@^5.0.5: + version "5.0.6" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.6.tgz#ec68fe0a641a29d8711579caf641d05bae1f2285" + integrity sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g== + dependencies: + balanced-match "^4.0.2" + cborg@^4.0.0: version "4.5.8" resolved "https://registry.yarnpkg.com/cborg/-/cborg-4.5.8.tgz#49cf664711237e4eb6197c841a2de79163e58c62" @@ -285,6 +512,27 @@ convert-source-map@^2.0.0: resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.3.1, debug@^4.3.2, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + detect-libc@^2.0.3: version "2.1.2" resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz" @@ -295,6 +543,95 @@ es-module-lexer@^2.0.0: resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz" integrity sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw== +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-9.1.2.tgz#b9de6ace2fab1cff24d2e58d85b74c8fcea39802" + integrity sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ== + dependencies: + "@types/esrecurse" "^4.3.1" + "@types/estree" "^1.0.8" + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be" + integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA== + +eslint@10.4.1: + version "10.4.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.4.1.tgz#f6640b176e0912246d9ddbf8fcfa5e8b7f02445a" + integrity sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw== + dependencies: + "@eslint-community/eslint-utils" "^4.8.0" + "@eslint-community/regexpp" "^4.12.2" + "@eslint/config-array" "^0.23.5" + "@eslint/config-helpers" "^0.6.0" + "@eslint/core" "^1.2.1" + "@eslint/plugin-kit" "^0.7.2" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" + ajv "^6.14.0" + cross-spawn "^7.0.6" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^9.1.2" + eslint-visitor-keys "^5.0.1" + espree "^11.2.0" + esquery "^1.7.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + json-stable-stringify-without-jsonify "^1.0.1" + minimatch "^10.2.4" + natural-compare "^1.4.0" + optionator "^0.9.3" + +espree@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-11.2.0.tgz#01d5e47dc332aaba3059008362454a8cc34ccaa5" + integrity sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw== + dependencies: + acorn "^8.16.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^5.0.1" + +esquery@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" + integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + estree-walker@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz" @@ -302,11 +639,31 @@ estree-walker@^3.0.3: dependencies: "@types/estree" "^1.0.0" +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + expect-type@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz" integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + fdir@^6.5.0: version "6.5.0" resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz" @@ -317,11 +674,66 @@ fflate@^0.8.0: resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + +flatted@^3.2.9: + version "3.4.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" + integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== + fsevents@~2.3.3: version "2.3.3" resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +globals@17.6.0: + version "17.6.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-17.6.0.tgz#0f0be018d5cca8690e6375ead1f65c4bb96191fc" + integrity sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA== + +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +ignore@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" + integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + ipfs-unixfs@^12.0.0: version "12.0.0" resolved "https://registry.yarnpkg.com/ipfs-unixfs/-/ipfs-unixfs-12.0.0.tgz#3b62c872fa8bd0fe820d7061a6edbff805053acf" @@ -330,11 +742,58 @@ ipfs-unixfs@^12.0.0: protons-runtime "^5.5.0" uint8arraylist "^2.4.8" +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.0, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + it-all@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/it-all/-/it-all-1.0.6.tgz#852557355367606295c4c3b7eff0136f07749335" integrity sha512-3cmCc6Heqe3uWi3CVM/k51fa/XbMFpQVzFoDsV0IZNHSQDyAXl3c4MjHkFX5kF3922OGj7Myv1nSEUgRtcuM1A== +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + lightningcss-android-arm64@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968" @@ -409,6 +868,13 @@ lightningcss@^1.32.0: lightningcss-win32-arm64-msvc "1.32.0" lightningcss-win32-x64-msvc "1.32.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + magic-string@^0.30.21: version "0.30.21" resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz" @@ -416,6 +882,18 @@ magic-string@^0.30.21: dependencies: "@jridgewell/sourcemap-codec" "^1.5.5" +minimatch@^10.2.2, minimatch@^10.2.4: + version "10.2.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.5.tgz#bd48687a0be38ed2961399105600f832095861d1" + integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== + dependencies: + brace-expansion "^5.0.5" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + multiformats@^13.0.0, multiformats@^13.1.0, multiformats@^13.3.1: version "13.4.2" resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-13.4.2.tgz#309ee17d3946db9a6954cf6832aeb2b4b10dd0f3" @@ -426,6 +904,11 @@ nanoid@^3.3.11: resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + numcodecs@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/numcodecs/-/numcodecs-0.3.2.tgz#09887cfc2a3ae1c59a495c01a7f0528118d85dcd" @@ -438,6 +921,42 @@ obug@^2.1.1: resolved "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz" integrity sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ== +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + pathe@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz" @@ -462,6 +981,11 @@ postcss@^8.5.8: picocolors "^1.1.1" source-map-js "^1.2.1" +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + protons-runtime@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/protons-runtime/-/protons-runtime-5.6.0.tgz#df1ef57497d04529a23ee6020f1c24f1d15db141" @@ -471,6 +995,11 @@ protons-runtime@^5.5.0: uint8arraylist "^2.4.3" uint8arrays "^5.0.1" +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + reference-spec-reader@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reference-spec-reader/-/reference-spec-reader-0.2.0.tgz#52bd79614dde68e68f05c97a05ae04ff20acd7ec" @@ -500,6 +1029,23 @@ rolldown@1.0.0-rc.9: "@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.9" "@rolldown/binding-win32-x64-msvc" "1.0.0-rc.9" +semver@^7.7.3: + version "7.8.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.3.tgz#c350de61c2a1ddbc96d7fae3e5b6fcf92d477fbe" + integrity sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + siginfo@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz" @@ -543,11 +1089,33 @@ tinyrainbow@^3.0.3: resolved "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz" integrity sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw== +ts-api-utils@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1" + integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA== + tslib@^2.4.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +typescript-eslint@8.61.0: + version "8.61.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.0.tgz#6927fb94f5f29623e370d33fd9fa61f15d6d996b" + integrity sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw== + dependencies: + "@typescript-eslint/eslint-plugin" "8.61.0" + "@typescript-eslint/parser" "8.61.0" + "@typescript-eslint/typescript-estree" "8.61.0" + "@typescript-eslint/utils" "8.61.0" + typescript@^5.4.0: version "5.9.3" resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" @@ -582,6 +1150,13 @@ unzipit@1.4.3: dependencies: uzip-module "^1.0.2" +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + uzip-module@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/uzip-module/-/uzip-module-1.0.3.tgz#6bbabe2a3efea5d5a4a47479f523a571de3427ce" @@ -627,6 +1202,13 @@ vitest@^4.1.0: vite "^6.0.0 || ^7.0.0 || ^8.0.0-0" why-is-node-running "^2.3.0" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + why-is-node-running@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz" @@ -635,6 +1217,16 @@ why-is-node-running@^2.3.0: siginfo "^2.0.0" stackback "0.0.2" +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + zarrita@^0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/zarrita/-/zarrita-0.5.4.tgz#b64a323c6f6c33e7f3cf741f70d5f48b31a7fb5a"