diff --git a/.gitignore b/.gitignore index 3e05106..900e842 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ .vscode/ -/target \ No newline at end of file +/target +js/deno_lockfile_wasm_bg.wasm +js/deno_lockfile_wasm.generated.d.ts +js/deno_lockfile_wasm.generated.js +js/LICENSE diff --git a/Cargo.lock b/Cargo.lock index fb464ec..3f47ac2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "deno_lockfile" version = "0.20.0" @@ -12,6 +24,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "deno_lockfile_wasm" +version = "0.0.0" +dependencies = [ + "deno_lockfile", + "serde", + "serde-wasm-bindgen", + "wasm-bindgen", +] + [[package]] name = "diff" version = "0.1.13" @@ -24,6 +46,27 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "pretty_assertions" version = "1.4.0" @@ -67,6 +110,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.159" @@ -126,6 +180,60 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 06adc18..1bfc34e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,12 @@ license = "MIT" description = "An implementation of a lockfile used in Deno" repository = "https://github.com/denoland/deno_lockfile" +[workspace] +members = ["lib"] + +[lib] +name = "deno_lockfile" + [dependencies] serde = { version = "1.0.149", features = ["derive"] } serde_json = "1.0.85" diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..7ad0105 --- /dev/null +++ b/deno.json @@ -0,0 +1,19 @@ +{ + "tasks": { + "build": "deno task pre-build && deno task wasmbuild --project deno_lockfile_wasm --out ./js", + "pre-build": "cp LICENSE js/LICENSE", + "test": "deno test --allow-read", + "test:fast": "deno task test --no-check --parallel --shuffle", + "wasmbuild": "deno run -A jsr:@deno/wasmbuild@0.17.2" + }, + "workspace": [ + "./js" + ], + "imports": { + "@std/assert": "jsr:@std/assert@^0.226.0", + "@std/testing": "jsr:@std/testing@^0.225.2" + }, + "exclude": [ + "./target" + ] +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..84f949e --- /dev/null +++ b/deno.lock @@ -0,0 +1,34 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@std/assert@^0.226.0": "jsr:@std/assert@0.226.0", + "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.0", + "jsr:@std/testing@^0.225.2": "jsr:@std/testing@0.225.2" + }, + "jsr": { + "@std/assert@0.226.0": { + "integrity": "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3", + "dependencies": [ + "jsr:@std/internal@^1.0.0" + ] + }, + "@std/internal@1.0.0": { + "integrity": "ac6a6dfebf838582c4b4f61a6907374e27e05bedb6ce276e0f1608fe84e7cd9a" + }, + "@std/testing@0.225.2": { + "integrity": "ae5a55e412926acdba98ec3b72aba93d2ab40cad3d0cd2454b048c10ca3584d8" + } + } + }, + "remote": {}, + "workspace": { + "dependencies": [ + "jsr:@std/assert@^0.226.0", + "jsr:@std/testing@^0.225.2" + ], + "members": { + "@deno/lockfile": {} + } + } +} diff --git a/js/deno.json b/js/deno.json new file mode 100644 index 0000000..5e7a7dc --- /dev/null +++ b/js/deno.json @@ -0,0 +1,12 @@ +{ + "name": "@deno/lockfile", + "version": "0.0.0", + "exports": { + ".": "./mod.ts" + }, + "publish": { + "exclude": [ + "!**" + ] + } +} diff --git a/js/mod.ts b/js/mod.ts new file mode 100644 index 0000000..9601ecd --- /dev/null +++ b/js/mod.ts @@ -0,0 +1,86 @@ +import * as wasm from "./deno_lockfile_wasm.generated.js"; +import { type JsLockfile } from "./deno_lockfile_wasm.generated.d.ts"; + +export interface WorkspaceMemberConfig { + dependencies?: string[]; + packageJson?: { + dependencies: string[]; + }; +} + +export interface WorkspaceConfig extends WorkspaceMemberConfig { + members?: Record; +} + +export interface LockfileJson { + version: string; + packages?: { + specifiers: Record; + jsr?: Record; + npm?: Record; + }; + redirects?: Record; + remote: Record; + workspace?: WorkspaceConfig; +} + +export interface JsrPackageInfo { + integrity: string; + dependencies?: string[]; +} + +export interface NpmPackageInfo { + integrity: string; + dependencies: Record; +} + +export interface Lockfile extends Omit { + insertNpmPackage(name: string, packageInfo: NpmPackageInfo): void; + setWorkspaceConfig(config: WorkspaceConfig): void; + toJson(): LockfileJson; + get filename(): string; +} + +export async function parseFromJson( + baseUrl: string | URL, + json: string | LockfileJson, +): Promise { + return parseFromJsonWith(baseUrl, json, await wasm.instantiate()); +} + +export interface InstanciateResult { + parseFromJson(baseUrl: string | URL, json: string | LockfileJson): Lockfile; +} + +export async function instantiate( + opts?: wasm.InstantiateOptions, +): Promise { + const mod = await wasm.instantiate(opts); + return { + parseFromJson(baseUrl, json) { + return parseFromJsonWith(baseUrl, json, mod); + }, + }; +} + +function parseFromJsonWith( + baseUrl: string | URL, + json: string | LockfileJson, + mod: Awaited>, +): Lockfile { + if (baseUrl instanceof URL) { + baseUrl = baseUrl.toString(); + } + if (typeof json === "object") { + json = JSON.stringify(json); + } + const inner = mod.parseFromJson(baseUrl, json); + return new Proxy(inner, { + get(target, prop, receiver) { + if (prop === "filename") { + return inner.filename(); + } + return Reflect.get(target, prop, receiver); + }, + }) as unknown as Lockfile; +} diff --git a/js/test.ts b/js/test.ts new file mode 100644 index 0000000..8489010 --- /dev/null +++ b/js/test.ts @@ -0,0 +1,287 @@ +import { + assertEquals, + assertExists, + assertFalse, + assertObjectMatch, +} from "@std/assert"; +import { beforeEach, describe, it } from "@std/testing/bdd"; +import { instantiate, Lockfile, parseFromJson } from "./mod.ts"; + +describe("parseFromJson", () => { + const json = { + version: "3", + packages: { + "specifiers": { + "jsr:@std/testing@^0.225.2": "jsr:@std/testing@0.225.2", + }, + jsr: { + "@std/testing@0.225.2": { + "integrity": + "ae5a55e412926acdba98ec3b72aba93d2ab40cad3d0cd2454b048c10ca3584d8", + }, + }, + }, + remote: {}, + }; + + it("should parse a LockFileJson and return a LockFile", async () => { + const lockfile = await parseFromJson( + "file:///deno.lock", + json, + ); + assertExists(lockfile); + }); + + it("should parse a stringified LockFileJson and return a LockFile", async () => { + const lockfile = await parseFromJson( + "file:///deno.lock", + JSON.stringify(json), + ); + assertExists(lockfile); + }); +}); + +describe("instantiate", () => { + it("should return a synchronous interface to parseFromJson", async () => { + const wasm = await instantiate(); + assertEquals( + wasm.parseFromJson("file:///deno.lock", { + version: "3", + remote: {}, + }).toJson(), + { + version: "3", + remote: {}, + }, + ); + }); +}); + +describe("LockFile", () => { + describe("filename", () => { + it("should return the filename", async () => { + const lockfile = await parseFromJson("file:///deno.lock", { + version: "3", + remote: {}, + }); + assertEquals(lockfile.filename, "file:///deno.lock"); + }); + }); + + describe("setWorkspaceConfig", () => { + let lockfile: Lockfile; + + beforeEach(async () => { + lockfile = await parseFromJson( + "file:///deno.lock", + { + version: "3", + packages: { + specifiers: { + "jsr:@std/assert@^0.226.0": "jsr:@std/assert@0.226.0", + "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.0", + "jsr:@std/testing@^0.225.2": "jsr:@std/testing@0.225.2", + }, + jsr: { + "@std/assert@0.226.0": { + integrity: + "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3", + dependencies: [ + "jsr:@std/internal@^1.0.0", + ], + }, + "@std/internal@1.0.0": { + integrity: + "ac6a6dfebf838582c4b4f61a6907374e27e05bedb6ce276e0f1608fe84e7cd9a", + }, + "@std/testing@0.225.2": { + integrity: + "ae5a55e412926acdba98ec3b72aba93d2ab40cad3d0cd2454b048c10ca3584d8", + }, + }, + }, + remote: {}, + workspace: { + dependencies: [ + "jsr:@std/assert@^0.226.0", + "jsr:@std/testing@^0.225.2", + ], + members: { + "@deno/lockfile": {}, + }, + }, + }, + ); + }); + + it("should remove all dependencies from a lockfile", () => { + lockfile.setWorkspaceConfig({ dependencies: [] }); + const actual = lockfile.toJson(); + assertFalse("packages" in actual); + assertFalse("workspace" in actual); + }); + + it("should retain a specific dependency from a lockfile", () => { + lockfile.setWorkspaceConfig({ + dependencies: ["jsr:@std/assert@^0.226.0"], + }); + const actual = lockfile.toJson(); + assertObjectMatch(actual, { + version: "3", + packages: { + specifiers: { + "jsr:@std/assert@^0.226.0": "jsr:@std/assert@0.226.0", + "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.0", + }, + jsr: { + "@std/assert@0.226.0": { + integrity: + "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3", + dependencies: ["jsr:@std/internal@^1.0.0"], + }, + "@std/internal@1.0.0": { + integrity: + "ac6a6dfebf838582c4b4f61a6907374e27e05bedb6ce276e0f1608fe84e7cd9a", + }, + }, + }, + remote: {}, + workspace: { dependencies: ["jsr:@std/assert@^0.226.0"] }, + }); + }); + }); + + describe("insertRemote", () => { + it("should insert a remote dependency", async () => { + const lockfile = await parseFromJson("file:///deno.lock", { + version: "3", + remote: {}, + }); + lockfile.insertRemote("https://deno.land/std@0.224.0/version.ts", "xxx"); + assertObjectMatch(lockfile.toJson(), { + version: "3", + remote: { + "https://deno.land/std@0.224.0/version.ts": "xxx", + }, + }); + }); + }); + + describe("insertNpmPackage", () => { + it("should insert an npm package", async () => { + const lockfile = await parseFromJson("file:///deno.lock", { + version: "3", + remote: {}, + }); + const npmPackageInfo = { + "integrity": + "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "isexe@2.0.0", + }, + }; + lockfile.insertNpmPackage("which@2.0.2", npmPackageInfo); + assertObjectMatch(lockfile.toJson(), { + packages: { + npm: { + "which@2.0.2": npmPackageInfo, + }, + }, + }); + }); + }); + + describe("insertPackageSpecifier", () => { + it("should insert a jsr package specifier", async () => { + const lockfile = await parseFromJson("file:///deno.lock", { + version: "3", + remote: {}, + }); + lockfile.insertPackageSpecifier( + "jsr:@std/testing@^0.225.0", + "jsr:@std/testing@0.225.2", + ); + assertObjectMatch(lockfile.toJson(), { + packages: { + specifiers: { + "jsr:@std/testing@^0.225.0": "jsr:@std/testing@0.225.2", + }, + }, + }); + }); + + it("should insert a npm package specifier", async () => { + const lockfile = await parseFromJson("file:///deno.lock", { + version: "3", + remote: {}, + }); + lockfile.insertPackageSpecifier("npm:which@^2.0.0", "npm:which@2.0.2"); + assertObjectMatch(lockfile.toJson(), { + packages: { + specifiers: { + "npm:which@^2.0.0": "npm:which@2.0.2", + }, + }, + }); + }); + }); + + describe("insertPackage", () => { + it("should insert a jsr package", async () => { + const lockfile = await parseFromJson("file:///deno.lock", { + version: "3", + remote: {}, + }); + const specifier = "jsr:@std/assert@0.226.0"; + const integrity = + "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3"; + lockfile.insertPackage(specifier, integrity); + assertObjectMatch(lockfile.toJson(), { + packages: { + jsr: { + [specifier]: { integrity }, + }, + }, + }); + }); + }); + + describe("addPackageDeps", () => { + it("should add dependencies of a jsr package", async () => { + const lockfile = await parseFromJson("file:///deno.lock", { + version: "3", + remote: {}, + }); + const specifier = "jsr:@std/assert@0.226.0"; + const integrity = + "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3"; + const dependencies = ["@std/internal@^1.0.0"]; + lockfile.insertPackage(specifier, integrity); + lockfile.addPackageDeps(specifier, dependencies); + assertObjectMatch(lockfile.toJson(), { + packages: { + jsr: { + [specifier]: { integrity, dependencies }, + }, + }, + }); + }); + }); + + describe("insertRedirect", () => { + it("should insert a redirect", async () => { + const lockfile = await parseFromJson("file:///deno.lock", { + version: "3", + remote: {}, + }); + const from = "https://deno.land/x/std/mod.ts"; + const to = "https://deno.land/std@0.224.0/mod.ts"; + lockfile.insertRedirect(from, to); + assertObjectMatch(lockfile.toJson(), { + redirects: { + [from]: to, + }, + }); + }); + }); +}); diff --git a/lib/Cargo.toml b/lib/Cargo.toml new file mode 100644 index 0000000..e7e47f5 --- /dev/null +++ b/lib/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "deno_lockfile_wasm" +version = "0.0.0" +edition = "2021" +homepage = "https://deno.land/" +repository = "https://github.com/denoland/deno_lockfile" +documentation = "https://docs.rs/deno_lockfile" +authors = ["the Deno authors"] +license = "MIT" + +[lib] +name = "deno_lockfile_wasm" +path = "lib.rs" +crate_type = ["cdylib"] + +[dependencies] +deno_lockfile = { path = "../" } +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.4" +wasm-bindgen = "=0.2.92" diff --git a/lib/lib.rs b/lib/lib.rs new file mode 100644 index 0000000..807aa2c --- /dev/null +++ b/lib/lib.rs @@ -0,0 +1,117 @@ +// Copyright 2024 the Deno authors. MIT license. + +use std::path::PathBuf; + +use deno_lockfile::Lockfile; +use deno_lockfile::NpmPackageDependencyLockfileInfo; +use deno_lockfile::NpmPackageInfo; +use deno_lockfile::NpmPackageLockfileInfo; +use deno_lockfile::SetWorkspaceConfigOptions; +use deno_lockfile::WorkspaceConfig; + +use serde::Serialize; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct JsLockfile(Lockfile); + +#[wasm_bindgen] +impl JsLockfile { + #[wasm_bindgen(constructor)] + pub fn new(filename: String, overwrite: bool) -> Self { + JsLockfile(Lockfile::new_empty(PathBuf::from(filename), overwrite)) + } + + #[wasm_bindgen(js_name = filename)] + pub fn filename(&self) -> String { + self.0.filename.display().to_string() + } + + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + self.0.as_json_string() + } + + #[wasm_bindgen(js_name = toJson)] + pub fn to_json(&self) -> JsValue { + let serializer = + serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true); + self.0.content.serialize(&serializer).unwrap() + } + + #[wasm_bindgen(js_name = setWorkspaceConfig)] + pub fn set_workspace_config(&mut self, config: JsValue) { + let config: WorkspaceConfig = + serde_wasm_bindgen::from_value(config).unwrap(); + self.0.set_workspace_config(SetWorkspaceConfigOptions { + config, + no_config: false, + no_npm: false, + }); + } + + #[wasm_bindgen(js_name = insertRemote)] + pub fn insert_remote(&mut self, specifier: String, hash: String) { + self.0.insert_remote(specifier, hash); + } + + #[wasm_bindgen(js_name = insertNpmPackage)] + pub fn insert_npm_package( + &mut self, + specifier: String, + package_info: JsValue, + ) { + let package_info: NpmPackageInfo = + serde_wasm_bindgen::from_value(package_info).unwrap(); + + let dependencies = package_info + .dependencies + .into_iter() + .map(|(k, v)| NpmPackageDependencyLockfileInfo { name: k, id: v }) + .collect(); + + self.0.insert_npm_package(NpmPackageLockfileInfo { + serialized_id: specifier, + integrity: package_info.integrity, + dependencies, + }); + } + + #[wasm_bindgen(js_name = insertPackageSpecifier)] + pub fn insert_package_specifier( + &mut self, + requirement: String, + identifier: String, + ) { + self.0.insert_package_specifier(requirement, identifier); + } + + #[wasm_bindgen(js_name = insertPackage)] + pub fn insert_package(&mut self, name: String, integrity: String) { + self.0.insert_package(name, integrity); + } + + #[wasm_bindgen(js_name = addPackageDeps)] + pub fn add_package_deps( + &mut self, + specifier: &str, + dependencies: Vec, + ) { + self.0.add_package_deps(specifier, dependencies.into_iter()); + } + + #[wasm_bindgen(js_name = insertRedirect)] + pub fn insert_redirect(&mut self, from: String, to: String) { + self.0.insert_redirect(from, to); + } +} + +#[wasm_bindgen(js_name = parseFromJson)] +pub fn js_parse_from_json( + filename: String, + content: &str, +) -> Result { + Lockfile::with_lockfile_content(PathBuf::from(filename), content, false) + .map(|lockfile| JsLockfile(lockfile)) + .map_err(|err| JsError::new(&err.to_string())) +}