From 3f484aeb413e2b18a090e3612ed8e70ef37144bc Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Tue, 4 Mar 2025 10:41:09 +0100 Subject: [PATCH] feat(js): add Gateway object (#1130) --- Cargo.lock | 7 - js-rattler/Cargo.lock | 14 ++ js-rattler/Cargo.toml | 5 + js-rattler/crate/error.rs | 10 +- js-rattler/crate/gateway.rs | 151 +++++++++++++++++++ js-rattler/crate/lib.rs | 1 + js-rattler/src/Gateway.test.ts | 64 ++++++++ js-rattler/src/Gateway.ts | 92 ++++++++++++ js-rattler/src/PackageName.test.ts | 44 ++++++ js-rattler/src/PackageName.ts | 226 +++++++++++++++++++++++++++++ js-rattler/src/Platform.ts | 137 +++++++++++++++++ js-rattler/src/index.ts | 3 + js-rattler/src/solve.test.ts | 4 +- js-rattler/src/solve.ts | 21 ++- js-rattler/src/typeUtils.ts | 61 ++++++++ 15 files changed, 828 insertions(+), 12 deletions(-) create mode 100644 js-rattler/crate/gateway.rs create mode 100644 js-rattler/src/Gateway.test.ts create mode 100644 js-rattler/src/Gateway.ts create mode 100644 js-rattler/src/PackageName.test.ts create mode 100644 js-rattler/src/PackageName.ts create mode 100644 js-rattler/src/Platform.ts create mode 100644 js-rattler/src/typeUtils.ts diff --git a/Cargo.lock b/Cargo.lock index 5915dd13c..c9ca34d54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6462,13 +6462,6 @@ dependencies = [ "wit-bindgen-rt", ] -[[package]] -name = "wasm-bin" -version = "0.1.0" -dependencies = [ - "rattler_solve", -] - [[package]] name = "wasm-bindgen" version = "0.2.100" diff --git a/js-rattler/Cargo.lock b/js-rattler/Cargo.lock index a47c0f73c..970950459 100644 --- a/js-rattler/Cargo.lock +++ b/js-rattler/Cargo.lock @@ -1545,7 +1545,10 @@ dependencies = [ "rattler_conda_types", "rattler_repodata_gateway", "rattler_solve", + "serde", + "serde-wasm-bindgen", "thiserror 2.0.11", + "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -2928,6 +2931,17 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.218" diff --git a/js-rattler/Cargo.toml b/js-rattler/Cargo.toml index cb9829fe7..a3643f60e 100644 --- a/js-rattler/Cargo.toml +++ b/js-rattler/Cargo.toml @@ -13,6 +13,9 @@ path = "crate/lib.rs" default = ["console_error_panic_hook"] [dependencies] +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.6.5" + wasm-bindgen = "0.2.95" wasm-bindgen-futures = "0.4.50" @@ -35,6 +38,8 @@ rattler_conda_types = { path = "../crates/rattler_conda_types" } rattler_repodata_gateway = { path = "../crates/rattler_repodata_gateway", features = ["gateway"] } rattler_solve = { path = "../crates/rattler_solve", default-features = false, features = ["resolvo"] } +url = "2.5.4" + # By adding the `libbz2-rs-sys` feature we ensure that bzip2 is using the rust # implementation. This is important because the C implementation is not # compatible with wasm. diff --git a/js-rattler/crate/error.rs b/js-rattler/crate/error.rs index bd3f86e49..040860e25 100644 --- a/js-rattler/crate/error.rs +++ b/js-rattler/crate/error.rs @@ -1,7 +1,7 @@ use rattler_conda_types::version_spec::ParseVersionSpecError; use rattler_conda_types::{ - ParseChannelError, ParseMatchSpecError, ParsePlatformError, ParseVersionError, - VersionBumpError, VersionExtendError, + InvalidPackageNameError, ParseChannelError, ParseMatchSpecError, ParsePlatformError, + ParseVersionError, VersionBumpError, VersionExtendError, }; use rattler_repodata_gateway::GatewayError; use rattler_solve::SolveError; @@ -28,6 +28,10 @@ pub enum JsError { GatewayError(#[from] GatewayError), #[error(transparent)] SolveError(#[from] SolveError), + #[error(transparent)] + Serde(#[from] serde_wasm_bindgen::Error), + #[error(transparent)] + PackageNameError(#[from] InvalidPackageNameError), } pub type JsResult = Result; @@ -44,6 +48,8 @@ impl From for JsValue { JsError::ParseMatchSpec(error) => JsValue::from_str(&error.to_string()), JsError::GatewayError(error) => JsValue::from_str(&error.to_string()), JsError::SolveError(error) => JsValue::from_str(&error.to_string()), + JsError::PackageNameError(error) => JsValue::from_str(&error.to_string()), + JsError::Serde(error) => error.into(), } } } diff --git a/js-rattler/crate/gateway.rs b/js-rattler/crate/gateway.rs new file mode 100644 index 000000000..bb0a1e00e --- /dev/null +++ b/js-rattler/crate/gateway.rs @@ -0,0 +1,151 @@ +use std::{collections::HashMap, path::PathBuf, str::FromStr}; + +use rattler_conda_types::{Channel, Platform}; +use rattler_repodata_gateway::{fetch::CacheAction, ChannelConfig, Gateway, SourceConfig}; +use serde::Deserialize; +use url::Url; +use wasm_bindgen::prelude::*; + +use crate::JsResult; + +#[wasm_bindgen] +#[repr(transparent)] +#[derive(Clone)] +pub struct JsGateway { + inner: Gateway, +} + +impl From for JsGateway { + fn from(value: Gateway) -> Self { + JsGateway { inner: value } + } +} + +impl From for Gateway { + fn from(value: JsGateway) -> Self { + value.inner + } +} + +impl AsRef for JsGateway { + fn as_ref(&self) -> &Gateway { + &self.inner + } +} + +#[derive(Default, Deserialize)] +#[serde(rename_all = "camelCase")] +struct JsGatewayOptions { + max_concurrent_requests: Option, + + #[serde(default)] + channel_config: JsChannelConfig, +} + +#[derive(Default, Deserialize)] +#[serde(rename_all = "camelCase")] +struct JsChannelConfig { + #[serde(default)] + default: JsSourceConfig, + #[serde(default)] + per_channel: HashMap, +} + +impl From for ChannelConfig { + fn from(value: JsChannelConfig) -> Self { + ChannelConfig { + default: value.default.into(), + per_channel: value + .per_channel + .into_iter() + .map(|(key, value)| (key, value.into())) + .collect(), + } + } +} + +fn yes() -> bool { + true +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct JsSourceConfig { + #[serde(default = "yes")] + zstd_enabled: bool, + + #[serde(default = "yes")] + bz2_enabled: bool, + + #[serde(default = "yes")] + sharded_enabled: bool, +} + +impl Default for JsSourceConfig { + fn default() -> Self { + Self { + zstd_enabled: true, + bz2_enabled: true, + sharded_enabled: true, + } + } +} + +impl From for SourceConfig { + fn from(value: JsSourceConfig) -> Self { + Self { + jlap_enabled: false, + zstd_enabled: value.zstd_enabled, + bz2_enabled: value.bz2_enabled, + sharded_enabled: value.sharded_enabled, + cache_action: CacheAction::default(), + } + } +} + +#[wasm_bindgen] +impl JsGateway { + #[wasm_bindgen(constructor)] + pub fn new(input: JsValue) -> JsResult { + let mut builder = Gateway::builder(); + let options: Option = serde_wasm_bindgen::from_value(input)?; + if let Some(options) = options { + if let Some(max_concurrent_requests) = options.max_concurrent_requests { + builder.set_max_concurrent_requests(max_concurrent_requests); + } + builder.set_channel_config(options.channel_config.into()); + }; + + Ok(Self { + inner: builder.finish(), + }) + } + + pub async fn names( + &self, + channels: Vec, + platforms: Vec, + ) -> Result, JsError> { + // TODO: Dont hardcode + let channel_config = + rattler_conda_types::ChannelConfig::default_with_root_dir(PathBuf::from("")); + + let channels = channels + .into_iter() + .map(|s| Channel::from_str(&s, &channel_config)) + .collect::, _>>()?; + let platforms = platforms + .into_iter() + .map(|p| Platform::from_str(&p)) + .collect::, _>>()?; + + Ok(self + .inner + .names(channels, platforms) + .execute() + .await? + .into_iter() + .map(|name| name.as_source().to_string()) + .collect()) + } +} diff --git a/js-rattler/crate/lib.rs b/js-rattler/crate/lib.rs index 32826b6f8..a19461337 100644 --- a/js-rattler/crate/lib.rs +++ b/js-rattler/crate/lib.rs @@ -1,4 +1,5 @@ mod error; +mod gateway; pub mod solve; mod utils; mod version; diff --git a/js-rattler/src/Gateway.test.ts b/js-rattler/src/Gateway.test.ts new file mode 100644 index 000000000..f53f3e239 --- /dev/null +++ b/js-rattler/src/Gateway.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from "@jest/globals"; +import { Gateway } from "./Gateway"; + +describe("Gateway", () => { + describe("constructor", () => { + it("works without arguments", () => { + expect(() => new Gateway()).not.toThrowError(); + expect(() => new Gateway(null)).not.toThrowError(); + expect(() => new Gateway(undefined)).not.toThrowError(); + }); + it("throws on invalid arguments", () => { + expect(() => new Gateway(true as any)).toThrowError(); + }); + it("accepts an empty object", () => { + expect(() => new Gateway({})).not.toThrowError(); + }); + it("accepts null for maxConcurrentRequests", () => { + expect( + () => + new Gateway({ + maxConcurrentRequests: null, + }), + ).not.toThrowError(); + }); + it("accepts empty channelConfig", () => { + expect( + () => + new Gateway({ + channelConfig: {}, + }), + ).not.toThrowError(); + }); + it("accepts perChannel channelConfig", () => { + expect( + () => + new Gateway({ + channelConfig: { + default: {}, + perChannel: { + "https://prefix.dev": { + bz2Enabled: false, + shardedEnabled: false, + zstdEnabled: false, + }, + }, + }, + }), + ).not.toThrowError(); + }); + }); + describe("names", () => { + const gateway = new Gateway(); + it("can query prefix.dev", () => { + return gateway + .names( + ["https://prefix.dev/emscripten-forge-dev"], + ["noarch", "emscripten-wasm32"], + ) + .then((names) => { + expect(names.length).toBeGreaterThanOrEqual(177); + }); + }); + }); +}); diff --git a/js-rattler/src/Gateway.ts b/js-rattler/src/Gateway.ts new file mode 100644 index 000000000..ea67f5fad --- /dev/null +++ b/js-rattler/src/Gateway.ts @@ -0,0 +1,92 @@ +import { JsGateway } from "../pkg"; +import { Platform } from "./Platform"; +import { NormalizedPackageName } from "./PackageName"; + +export type GatewaySourceConfig = { + /** `true` if downloading `repodata.json.zst` is enabled. Defaults to `true`. */ + zstdEnabled?: boolean; + + /** `true` if downloading `repodata.json.bz2` is enabled. Defaults to `true`. */ + bz2Enabled?: boolean; + + /** + * `true` if sharded repodata is available for the channel. Defaults to + * `true`. + */ + shardedEnabled?: boolean; +}; + +export type GatewayChannelConfig = { + /** + * The default configuration for a channel if its is not explicitly matched + * in the `perChannel` field. + */ + default?: GatewaySourceConfig; + + /** + * Configuration for a specific channel. + * + * The key refers to the prefix of a channel so `https://prefix.dev` matches + * any channel on `https://prefix.dev`. The key with the longest match is + * used. + */ + perChannel?: { + [key: string]: GatewaySourceConfig; + }; +}; + +export type GatewayOptions = { + /** + * The maximum number of concurrent requests the gateway can execute. By + * default there is no limit. + */ + maxConcurrentRequests?: number | null; + + /** Defines how to access channels. */ + channelConfig?: GatewayChannelConfig; +}; + +/** + * A `Gateway` provides efficient access to conda repodata. + * + * Repodata can be accessed through several different methods. The `Gateway` + * implements all the nitty-gritty details of repodata access and provides a + * simple high level API for consumers. + * + * The Gateway efficiently manages memory to reduce it to the bare minimum. + * + * Internally the gateway caches all fetched repodata records, running the same + * query twice will return the previous results. + * + * @public + */ +export class Gateway { + /** @internal */ + native: JsGateway; + + /** + * Constructs a new Gateway object. + * + * @param options - The options to configure the Gateway with. + */ + constructor(options?: GatewayOptions | null) { + this.native = new JsGateway(options); + } + + /** + * Returns the names of the package that are available for the given + * channels and platforms. + * + * @param channels - The channels to query + * @param platforms - The platforms to query + */ + public async names( + channels: string[], + platforms: Platform[], + ): Promise { + return (await this.native.names( + channels, + platforms, + )) as NormalizedPackageName[]; + } +} diff --git a/js-rattler/src/PackageName.test.ts b/js-rattler/src/PackageName.test.ts new file mode 100644 index 000000000..059cbf030 --- /dev/null +++ b/js-rattler/src/PackageName.test.ts @@ -0,0 +1,44 @@ +import { IsTrue, IsSame } from "./typeUtils"; +import { + PackageNameLiteral, + isPackageName, + normalizePackageName, + isNormalizedPackageName, +} from "./PackageName"; +import { expect, test } from "@jest/globals"; + +type Test1 = IsTrue, "abc">>; +type Test2 = IsTrue, never>>; +type Test3 = IsTrue, never>>; + +test("isPackageName", () => { + expect(isPackageName("abc")).toBeTruthy(); + expect(isPackageName("foo-bar")).toBeTruthy(); + expect(isPackageName("foo_bar")).toBeTruthy(); + expect(isPackageName("foo_bar.baz")).toBeTruthy(); + expect(isPackageName("Fo0_B4R-BaZ.B0b")).toBeTruthy(); + + expect(isPackageName("")).toBeFalsy(); + expect(isPackageName("!")).toBeFalsy(); + expect(isPackageName(" ")).toBeFalsy(); + expect(isPackageName("$")).toBeFalsy(); +}); + +test("isNormalizedPackageName", () => { + expect(isNormalizedPackageName("abc")).toBeTruthy(); + expect(isNormalizedPackageName("foo-bar")).toBeTruthy(); + expect(isNormalizedPackageName("foo_bar")).toBeTruthy(); + expect(isNormalizedPackageName("foo_bar.baz")).toBeTruthy(); + + expect(isNormalizedPackageName("Fo0_B4R-BaZ.B0b")).toBeFalsy(); + expect(isNormalizedPackageName("!")).toBeFalsy(); + expect(isNormalizedPackageName(" ")).toBeFalsy(); + expect(isNormalizedPackageName("$")).toBeFalsy(); + expect(isNormalizedPackageName("")).toBeFalsy(); +}); + +test("normalizePackageName", () => { + expect(normalizePackageName("abc")).toBe("abc"); + expect(normalizePackageName("aBc")).toBe("abc"); + expect(normalizePackageName("Fo0_B4R-BaZ.B0b")).toBe("fo0_b4r-baz.b0b"); +}); diff --git a/js-rattler/src/PackageName.ts b/js-rattler/src/PackageName.ts new file mode 100644 index 000000000..ec78960d9 --- /dev/null +++ b/js-rattler/src/PackageName.ts @@ -0,0 +1,226 @@ +import { NonEmptyString } from "./typeUtils"; + +/** + * Defines the allowed characters for any package name. + * + * Allowed characters: + * + * - Lowercase letters (`a-z`) + * - Uppercase letters (`A-Z`) + * - Digits (`0-9`) + * - Underscore (`_`) + * - Dash (`-`) + * - Dot (`.`) + * + * @public + */ +// prettier-ignore +export type PackageNameChar = + | "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" + | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" + | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" + | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" + | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + | "_" + | "-" + | "."; + +/** + * Checks whether a string consists only of valid package name characters. + * + * - If `S` contains only allowed characters, it resolves to `S`. + * - Otherwise, it resolves to `never`. + * + * @example + * + * ```ts + * type Valid = ContainsOnlyPackageNameChars<"valid-name">; // 'valid-name' + * type Invalid = ContainsOnlyPackageNameChars<"invalid!">; // never + * ``` + * + * @public + */ +export type ContainsOnlyPackageNameChars = S extends "" + ? "" // Empty string is valid + : S extends `${infer First}${infer Rest}` + ? First extends PackageNameChar + ? ContainsOnlyPackageNameChars extends never + ? never + : S + : never + : never; + +/** + * Ensures that a string is a valid package name. + * + * - If `S` contains only valid characters and is not empty, it resolves to `S`. + * - Otherwise, it resolves to `never`. + * + * @example + * + * ```ts + * type Valid = PackageNameLiteral<"valid-name">; // 'valid-name' + * type Invalid = PackageNameLiteral<"invalid!">; // never + * type Empty = PackageNameLiteral<"">; // never + * ``` + * + * @public + */ +export type PackageNameLiteral = + ContainsOnlyPackageNameChars & NonEmptyString; + +/** A unique symbol used for branding `PackageName` types. */ +declare const PACKAGE_NAME_BRAND: unique symbol; + +/** + * A **branded type** representing a validated package name. + * + * - This type is **enforced at runtime** using `isPackageName()`. + * - Ensures that a package name conforms to the expected format. + * + * @example + * + * ```ts + * const pkg: PackageName = "valid-package" as PackageName; + * ``` + * + * @public + */ +export type PackageName = string & { [PACKAGE_NAME_BRAND]: void }; + +/** + * A **branded type** representing a **normalized** package name. + * + * - A `NormalizedPackageName` is always **lowercase**. + * - It extends `PackageName`, ensuring that it still follows package name rules. + * - Can be obtained by calling `normalizePackageName()`. + * + * @example + * + * ```ts + * const normalized: NormalizedPackageName = + * "valid-package" as NormalizedPackageName; + * ``` + * + * @public + */ +export type NormalizedPackageName = Lowercase; + +/** + * A type that accepts: + * + * - A **PackageName** (a runtime-validated string). + * - A **string literal** that satisfies `PackageNameLiteral`. + * + * This is useful for functions that accept both validated runtime values and + * compile-time checked literals. + * + * @example + * + * ```ts + * function processPackage(name: PackageNameOrLiteral) { ... } + * + * processPackage("valid-package"); // ✅ Allowed (checked at compile-time) + * processPackage("invalid!"); // ❌ Compile-time error + * ``` + * + * @param S - The input string type. + * @public + */ +export type PackageNameOrLiteral = + | PackageName + | (S extends PackageNameLiteral ? S : never); + +/** + * A type that accepts: + * + * - A **NormalizedPackageName** (a runtime-validated string). + * - A **string literal** that satisfies `Lowercase>`. + * + * This is useful for functions that accept both validated runtime values and + * compile-time checked literals. + * + * @example + * + * ```ts + * function processNormalizedPackage(name: NormalizedPackageNameOrLiteral) { ... } + * + * processNormalizedPackage("valid-package"); // ✅ Allowed (checked at compile-time) + * processNormalizedPackage("Invalid-Package"); // ❌ Compile-time error + * ``` + * + * @param S - The input string type. + * @public + */ +export type NormalizedPackageNameOrLiteral = + | NormalizedPackageName + | (S extends Lowercase> ? S : never); + +/** + * **Normalizes a package name to lowercase.** + * + * - If given a **string literal**, it is validated at compile time. + * - If given a **runtime-validated** `PackageName`, it is accepted directly. + * - Returns a `NormalizedPackageName` with all characters converted to lowercase. + * + * @example + * + * ```ts + * const normalized = normalizePackageName("Valid-Package"); // "valid-package" + * ``` + * + * @param name - The package name to normalize. + * @returns The normalized package name. + * @public + */ +export function normalizePackageName( + name: PackageNameOrLiteral, +): NormalizedPackageName { + return name.toLowerCase() as NormalizedPackageName; +} + +/** + * **Checks if a string is a valid `PackageName`.** + * + * - Returns `true` if `input` matches the allowed package name format. + * - If `true`, TypeScript narrows the type to `PackageName`. + * + * @example + * + * ```ts + * if (isPackageName(userInput)) { + * const validName: PackageName = userInput; + * } + * ``` + * + * @param input - The string to validate. + * @returns `true` if valid, otherwise `false`. + * @public + */ +export function isPackageName(input: string): input is PackageName { + return /^[A-Za-z0-9_.-]+$/.test(input); +} + +/** + * **Checks if a string is a valid `NormalizedPackageName`.** + * + * - A normalized package name must be **lowercase**. + * - If `true`, TypeScript narrows the type to `NormalizedPackageName`. + * + * @example + * + * ```ts + * if (isNormalizedPackageName(userInput)) { + * const validNormalizedName: NormalizedPackageName = userInput; + * } + * ``` + * + * @param input - The string to validate. + * @returns `true` if valid, otherwise `false`. + * @public + */ +export function isNormalizedPackageName( + input: string, +): input is NormalizedPackageName { + return /^[a-z0-9_.-]+$/.test(input); +} diff --git a/js-rattler/src/Platform.ts b/js-rattler/src/Platform.ts new file mode 100644 index 000000000..9001d1d63 --- /dev/null +++ b/js-rattler/src/Platform.ts @@ -0,0 +1,137 @@ +/** + * All platform names supported by this library. + * + * @public + */ +export const platformNames = [ + "noarch", + "linux-32", + "linux-64", + "linux-aarch64", + "linux-armv6l", + "linux-armv7l", + "linux-ppc64le", + "linux-ppc64", + "linux-ppc", + "linux-s390x", + "linux-riscv32", + "linux-riscv64", + "osx-64", + "osx-arm64", + "win-32", + "win-64", + "win-arm64", + "emscripten-wasm32", + "wasi-wasm32", + "zos-z", +] as const; + +/** + * A type that represents a valid platform. + * + * @public + */ +export type Platform = (typeof platformNames)[number]; + +/** + * A type guard that identifies if an input value is a `Platform` + * + * @public + */ +export function isPlatform(maybePlatform: unknown): maybePlatform is Platform { + return ( + typeof maybePlatform === "string" && + platformNames.includes(maybePlatform as Platform) + ); +} + +/** + * All architecture names supported by this library. + * + * @public + */ +export const archNames = [ + "x86", + "x86_64", + "aarch64", + "arm64", + "armv6l", + "armv7l", + "ppc64le", + "ppc64", + "ppc", + "s390x", + "riscv32", + "riscv64", + "wasm32", + "z", +] as const; + +/** + * A type that represents a valid architecture. + * + * @public + */ +export type Arch = (typeof archNames)[number]; + +/** + * A type guard that identifies if an input value is an `Arch` + * + * @public + */ +export function isArch(maybeArch: unknown): maybeArch is Platform { + return ( + typeof maybeArch === "string" && archNames.includes(maybeArch as Arch) + ); +} + +/** + * Returns the architecture of a certain platform + * + * @param platform - The platform + * @public + */ +export function platformArch(platform: Platform): Arch | null { + switch (platform) { + case "noarch": + return null; + case "linux-32": + return "x86"; + case "linux-64": + return "x86_64"; + case "linux-aarch64": + return "aarch64"; + case "linux-armv6l": + return "armv6l"; + case "linux-armv7l": + return "armv7l"; + case "linux-ppc64le": + return "ppc64le"; + case "linux-ppc64": + return "ppc64"; + case "linux-ppc": + return "ppc"; + case "linux-s390x": + return "s390x"; + case "linux-riscv32": + return "riscv32"; + case "linux-riscv64": + return "riscv64"; + case "osx-64": + return "x86_64"; + case "osx-arm64": + return "arm64"; + case "win-32": + return "x86"; + case "win-64": + return "x86_64"; + case "win-arm64": + return "arm64"; + case "emscripten-wasm32": + return "wasm32"; + case "wasi-wasm32": + return "wasm32"; + case "zos-z": + return "z"; + } +} diff --git a/js-rattler/src/index.ts b/js-rattler/src/index.ts index 29f95e84b..92044f5a5 100644 --- a/js-rattler/src/index.ts +++ b/js-rattler/src/index.ts @@ -1,4 +1,7 @@ export { ParseStrictness } from "../pkg/"; export * from "./Version"; export * from "./VersionSpec"; +export * from "./Platform"; export * from "./solve"; +export * from "./PackageName"; +export * from "./typeUtils"; diff --git a/js-rattler/src/solve.test.ts b/js-rattler/src/solve.test.ts index 6937096d9..4c0270ad0 100644 --- a/js-rattler/src/solve.test.ts +++ b/js-rattler/src/solve.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "@jest/globals"; -import { simple_solve } from "./solve"; +import { simpleSolve } from "./solve"; describe("solving", () => { it("python should yield three packages", () => { - return simple_solve( + return simpleSolve( ["python"], [ "https://prefix.dev/emscripten-forge-dev", diff --git a/js-rattler/src/solve.ts b/js-rattler/src/solve.ts index 69976fac0..6bdb8adb2 100644 --- a/js-rattler/src/solve.ts +++ b/js-rattler/src/solve.ts @@ -1 +1,20 @@ -export { simple_solve, SolvedPackage } from "../pkg"; +import { Platform } from "./Platform"; +import { simple_solve as wasm_simple_solve, SolvedPackage } from "../pkg"; + +export { SolvedPackage } from "../pkg"; + +/** + * Solve an environment + * + * @param specs - Matchspecs of packages that must be included. + * @param channels - The channels to request for repodata of packages + * @param platforms - The platforms to solve for + * @public + */ +export async function simpleSolve( + specs: string[], + channels: string[], + platforms: Platform[], +): Promise { + return await wasm_simple_solve(specs, channels, platforms); +} diff --git a/js-rattler/src/typeUtils.ts b/js-rattler/src/typeUtils.ts new file mode 100644 index 000000000..53d84b8af --- /dev/null +++ b/js-rattler/src/typeUtils.ts @@ -0,0 +1,61 @@ +/** + * Ensures that a given type is `true`. Used for compile-time assertions. + * + * @example + * + * ```ts + * type Test = IsTrue; // Passes + * type TestError = IsTrue; // Type error + * ``` + * + * @internal + */ +export type IsTrue = T; + +/** + * Ensures that a given type is `false`. Used for compile-time assertions. + * + * @example + * + * ```ts + * type Test = IsFalse; // Passes + * type TestError = IsFalse; // Type error + * ``` + * + * @internal + */ +export type IsFalse = T; + +/** + * Checks if two types `A` and `B` are exactly the same. + * + * - If `A` and `B` are identical, resolves to `true`. + * - Otherwise, resolves to `false`. + * + * @example + * + * ```ts + * type Same = IsSame<"foo", "foo">; // true + * type Different = IsSame<"foo", "bar">; // false + * ``` + * + * @internal + */ +export type IsSame = A extends B ? (B extends A ? true : false) : false; + +/** + * Ensures that a string is non-empty. + * + * - If `T` is an empty string, it resolves to `never`. + * - Otherwise, it resolves to `T`. + * + * @example + * + * ```ts + * type Valid = NonEmptyString<"hello">; // 'hello' + * type Invalid = NonEmptyString<"">; // never + * ``` + * + * @public + */ +export type NonEmptyString = T extends "" ? never : T;