From acde2a518f96b8a14ab658560e95fe93f72e4bb0 Mon Sep 17 00:00:00 2001 From: Michal Bajer Date: Tue, 24 Oct 2023 11:35:03 +0000 Subject: [PATCH] build(deps): replace ipfs-http-client with kubo-rpc-client - Replace deprecated ipfs-http-client with kubo-rpc-client. - kubo-rpc-client must be imported dynamically since it's ESM-only and we still use CJS. Depends on: #2821 Signed-off-by: Michal Bajer --- .cspell.json | 4 + .../package.json | 6 +- .../src/main/typescript/i-ipfs-http-client.ts | 48 ------ .../main/typescript/kubo-rpc-client-types.ts | 127 ++++++++++++++ .../typescript/plugin-object-store-ipfs.ts | 76 ++++++--- .../src/main/typescript/public-api.ts | 6 +- .../fixtures/mock/ipfs/ipfs-files-api-mock.ts | 157 ------------------ .../plugin-object-store-ipfs.test.ts | 20 ++- .../unit/plugin-object-store-ipfs.test.ts | 12 +- .../tsconfig.json | 3 + jest.config.js | 1 + packages/cacti-esm-compat-hacks/README.md | 17 ++ packages/cacti-esm-compat-hacks/package.json | 64 +++++++ .../src/main/typescript/dynamic-import.ts | 20 +++ .../src/main/typescript/index.ts | 1 + .../src/main/typescript/index.web.ts | 1 + .../src/main/typescript/public-api.ts | 1 + .../integration/api-surface.test.ts | 6 + packages/cacti-esm-compat-hacks/tsconfig.json | 14 ++ tsconfig.json | 3 + yarn.lock | 114 +++++++++++-- 21 files changed, 438 insertions(+), 263 deletions(-) delete mode 100644 extensions/cactus-plugin-object-store-ipfs/src/main/typescript/i-ipfs-http-client.ts create mode 100644 extensions/cactus-plugin-object-store-ipfs/src/main/typescript/kubo-rpc-client-types.ts delete mode 100644 extensions/cactus-plugin-object-store-ipfs/src/test/typescript/fixtures/mock/ipfs/ipfs-files-api-mock.ts create mode 100644 packages/cacti-esm-compat-hacks/README.md create mode 100644 packages/cacti-esm-compat-hacks/package.json create mode 100644 packages/cacti-esm-compat-hacks/src/main/typescript/dynamic-import.ts create mode 100755 packages/cacti-esm-compat-hacks/src/main/typescript/index.ts create mode 100755 packages/cacti-esm-compat-hacks/src/main/typescript/index.web.ts create mode 100755 packages/cacti-esm-compat-hacks/src/main/typescript/public-api.ts create mode 100644 packages/cacti-esm-compat-hacks/src/test/typescript/integration/api-surface.test.ts create mode 100644 packages/cacti-esm-compat-hacks/tsconfig.json diff --git a/.cspell.json b/.cspell.json index 7893e92eb4..6e71dd09dd 100644 --- a/.cspell.json +++ b/.cspell.json @@ -71,6 +71,8 @@ "ipaddress", "ipfs", "IPFSHTTP", + "IPLD", + "ipld", "Iroha", "Irohad", "isready", @@ -84,6 +86,7 @@ "KEYUTIL", "KJUR", "Knetic", + "kubo", "LEDGERBLOCKACK", "leveldb", "lmify", @@ -147,6 +150,7 @@ "txqueue", "Uisrs", "undici", + "unixfs", "Unmarshal", "uuidv", "vscc", diff --git a/extensions/cactus-plugin-object-store-ipfs/package.json b/extensions/cactus-plugin-object-store-ipfs/package.json index eb2ca7205f..f9fb0f3437 100644 --- a/extensions/cactus-plugin-object-store-ipfs/package.json +++ b/extensions/cactus-plugin-object-store-ipfs/package.json @@ -55,21 +55,23 @@ "webpack:dev:web": "webpack --env=dev --target=web --config ../../webpack.config.js" }, "dependencies": { + "@hyperledger/cacti-esm-compat-hacks": "2.0.0-alpha.2", "@hyperledger/cactus-common": "2.0.0-alpha.2", "@hyperledger/cactus-core": "2.0.0-alpha.2", "@hyperledger/cactus-core-api": "2.0.0-alpha.2", "axios": "1.5.1", - "ipfs-http-client": "60.0.1", "run-time-error": "1.4.0", "typescript-optional": "2.0.1", "uuid": "8.3.2" }, "devDependencies": { "@hyperledger/cactus-test-tooling": "2.0.0-alpha.2", + "@multiformats/multiaddr": "11.6.1", "@types/express": "4.17.19", "express": "4.18.2", "ipfs-core-types": "0.14.1", - "multiformats": "9.4.9" + "ipfs-unixfs": "9.0.1", + "multiformats": "11.0.2" }, "engines": { "node": ">=10", diff --git a/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/i-ipfs-http-client.ts b/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/i-ipfs-http-client.ts deleted file mode 100644 index 42a4325b6c..0000000000 --- a/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/i-ipfs-http-client.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { IPFS } from "ipfs-core-types"; -import type { EndpointConfig, IPFSHTTPClient } from "ipfs-http-client"; - -export interface IIpfsHttpClient extends IPFS { - getEndpointConfig: () => EndpointConfig; -} - -export function isIpfsHttpClientOptions(x: unknown): x is IPFSHTTPClient { - if (!x) { - return false; - } - return ( - typeof (x as IIpfsHttpClient).add === "function" && - typeof (x as IIpfsHttpClient).addAll === "function" && - typeof (x as IIpfsHttpClient).bitswap === "object" && - typeof (x as IIpfsHttpClient).block === "object" && - typeof (x as IIpfsHttpClient).bootstrap === "object" && - typeof (x as IIpfsHttpClient).cat === "function" && - typeof (x as IIpfsHttpClient).commands === "function" && - typeof (x as IIpfsHttpClient).config === "object" && - typeof (x as IIpfsHttpClient).dag === "object" && - typeof (x as IIpfsHttpClient).dht === "object" && - typeof (x as IIpfsHttpClient).diag === "object" && - typeof (x as IIpfsHttpClient).dns === "function" && - typeof (x as IIpfsHttpClient).files === "object" && - typeof (x as IIpfsHttpClient).get === "function" && - typeof (x as IIpfsHttpClient).getEndpointConfig === "function" && - typeof (x as IIpfsHttpClient).id === "function" && - typeof (x as IIpfsHttpClient).isOnline === "function" && - typeof (x as IIpfsHttpClient).key === "object" && - typeof (x as IIpfsHttpClient).log === "object" && - typeof (x as IIpfsHttpClient).ls === "function" && - typeof (x as IIpfsHttpClient).mount === "function" && - typeof (x as IIpfsHttpClient).name === "object" && - typeof (x as IIpfsHttpClient).object === "object" && - typeof (x as IIpfsHttpClient).pin === "object" && - typeof (x as IIpfsHttpClient).ping === "function" && - typeof (x as IIpfsHttpClient).pubsub === "object" && - // typeof (x as IIpfsHttpClient).refs === "function" && - typeof (x as IIpfsHttpClient).repo === "object" && - typeof (x as IIpfsHttpClient).resolve === "function" && - typeof (x as IIpfsHttpClient).start === "function" && - typeof (x as IIpfsHttpClient).stats === "object" && - typeof (x as IIpfsHttpClient).stop === "function" && - typeof (x as IIpfsHttpClient).swarm === "object" && - typeof (x as IIpfsHttpClient).version === "function" - ); -} diff --git a/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/kubo-rpc-client-types.ts b/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/kubo-rpc-client-types.ts new file mode 100644 index 0000000000..5f3a547b75 --- /dev/null +++ b/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/kubo-rpc-client-types.ts @@ -0,0 +1,127 @@ +/** + * Since kubo-rpc-client uses ESM only, we can't import it to get types (since we use CJS). + * To fix this we define required types here, based on their counterparts in kubo-rpc-client. + */ + +import type { Multiaddr } from "@multiformats/multiaddr"; +import type { MultihashHasher } from "multiformats/hashes/interface"; +import type { Agent as HttpAgent } from "http"; +import type { Agent as HttpsAgent } from "https"; +import type { CID } from "multiformats/cid"; +import type { Mtime } from "ipfs-unixfs"; + +///////////////////////////////////// +// Types from kubo-rpc-client +///////////////////////////////////// +// Some are simplified when details are not needed + +export type MultibaseCodec = + import("multiformats/bases/interface").MultibaseCodec; +export type BlockCodec< + T1 = any, + T2 = any, +> = import("multiformats/codecs/interface").BlockCodec; + +export interface LoadBaseFn { + (codeOrName: number | string): Promise>; +} +export interface LoadCodecFn { + (codeOrName: number | string): Promise>; +} +export interface LoadHasherFn { + (codeOrName: number | string): Promise; +} + +export interface IPLDOptions { + loadBase: LoadBaseFn; + loadCodec: LoadCodecFn; + loadHasher: LoadHasherFn; + bases: Array>; + codecs: Array>; + hashers: MultihashHasher[]; +} + +export interface Options { + host?: string; + port?: number; + protocol?: string; + headers?: Headers | Record; + timeout?: number | string; + apiPath?: string; + url?: URL | string | Multiaddr; + ipld?: Partial; + agent?: HttpAgent | HttpsAgent; +} + +export type IPFSPath = CID | string; + +export interface StatResult { + cid: CID; + size: number; + cumulativeSize: number; + type: "directory" | "file"; + blocks: number; + withLocality: boolean; + local?: boolean; + sizeLocal?: number; + mode?: number; + mtime?: Mtime; +} + +///////////////////////////////////////////////////////// +// LikeIpfsHttpClient instead of full IpfsHttpClient +///////////////////////////////////////////////////////// + +/** + * Connector only needs these methods to work. + * More methods can be added in the future. + */ +export interface LikeIpfsHttpClientFile { + read: ( + ipfsPath: IPFSPath, + options?: Record, + ) => AsyncIterable; + + write: ( + ipfsPath: string, + content: + | string + | Uint8Array + | Blob + | AsyncIterable + | Iterable, + options?: Record, + ) => Promise; + + stat: ( + ipfsPath: IPFSPath, + options?: Record, + ) => Promise; +} + +export function isLikeIpfsHttpClientFile( + x: unknown, +): x is LikeIpfsHttpClientFile { + if (!x) { + return false; + } + return ( + typeof (x as LikeIpfsHttpClientFile).read === "function" && + typeof (x as LikeIpfsHttpClientFile).write === "function" && + typeof (x as LikeIpfsHttpClientFile).stat === "function" + ); +} + +/** + * Only files API is used + */ +export interface LikeIpfsHttpClient { + files: LikeIpfsHttpClientFile; +} + +export function isLikeIpfsHttpClient(x: unknown): x is LikeIpfsHttpClient { + if (!x) { + return false; + } + return isLikeIpfsHttpClientFile((x as LikeIpfsHttpClient).files); +} diff --git a/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/plugin-object-store-ipfs.ts b/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/plugin-object-store-ipfs.ts index 185b956868..7c4bb20ee2 100644 --- a/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/plugin-object-store-ipfs.ts +++ b/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/plugin-object-store-ipfs.ts @@ -1,10 +1,12 @@ import path from "path"; import type { Express } from "express"; -import { create, IPFSHTTPClient } from "ipfs-http-client"; -import type { Options } from "ipfs-http-client"; import { RuntimeError } from "run-time-error"; -import { Logger, Checks, LoggerProvider } from "@hyperledger/cactus-common"; -import type { LogLevelDesc } from "@hyperledger/cactus-common"; +import { + Logger, + Checks, + LoggerProvider, + LogLevelDesc, +} from "@hyperledger/cactus-common"; import type { IPluginObjectStore, ICactusPluginOptions, @@ -16,13 +18,18 @@ import type { SetObjectRequestV1, SetObjectResponseV1, } from "@hyperledger/cactus-core-api"; +import { dynamicImportKuboRpcClientESMWorkaround } from "@hyperledger/cacti-esm-compat-hacks"; import OAS from "../json/openapi.json"; import { GetObjectEndpointV1 } from "./web-services/get-object-endpoint-v1"; import { SetObjectEndpointV1 } from "./web-services/set-object-endpoint-v1"; import { HasObjectEndpointV1 } from "./web-services/has-object-endpoint-v1"; -import { isIpfsHttpClientOptions } from "./i-ipfs-http-client"; +import { + LikeIpfsHttpClient, + isLikeIpfsHttpClient, + Options, +} from "./kubo-rpc-client-types"; export const K_IPFS_JS_HTTP_ERROR_FILE_DOES_NOT_EXIST = "HTTPError: file does not exist"; @@ -30,13 +37,13 @@ export const K_IPFS_JS_HTTP_ERROR_FILE_DOES_NOT_EXIST = export interface IPluginObjectStoreIpfsOptions extends ICactusPluginOptions { readonly logLevel?: LogLevelDesc; readonly parentDir: string; - readonly ipfsClientOrOptions: Options | IPFSHTTPClient; + readonly ipfsClientOrOptions: Options | LikeIpfsHttpClient; } export class PluginObjectStoreIpfs implements IPluginObjectStore { public static readonly CLASS_NAME = "PluginObjectStoreIpfs"; - private readonly ipfs: IPFSHTTPClient; + private ipfs: LikeIpfsHttpClient | undefined; private readonly log: Logger; private readonly instanceId: string; private readonly parentDir: string; @@ -45,6 +52,38 @@ export class PluginObjectStoreIpfs implements IPluginObjectStore { return PluginObjectStoreIpfs.CLASS_NAME; } + /** + * We use dynamic import for kubo-rpc-client since it's ESM and we can't import it normally. + * This methods will load the module and initialize local IPFS client based on ctor arguments. + */ + private async initIpfs(): Promise { + if (isLikeIpfsHttpClient(this.opts.ipfsClientOrOptions)) { + this.ipfs = this.opts.ipfsClientOrOptions; + } else if (this.opts.ipfsClientOrOptions) { + const kuboRpcModule = await dynamicImportKuboRpcClientESMWorkaround(); + this.ipfs = kuboRpcModule.create(this.opts.ipfsClientOrOptions); + } else { + const errorMessage = `initIpfs Need either "ipfsClient" or "ipfsClientOptions" to construct ${this.className} Neither was provided.`; + throw new RuntimeError(errorMessage); + } + } + + /** + * Get IPFS client or initialize it from constructor args. + * @returns `LikeIpfsHttpClient` or exception + */ + private async getIpfs(): Promise { + if (!this.ipfs) { + await this.initIpfs(); + } + + if (!this.ipfs) { + throw new Error("Could not instantiate ipfs http client"); + } + + return this.ipfs; + } + constructor(public readonly opts: IPluginObjectStoreIpfsOptions) { const fnTag = `${this.className}#constructor()`; Checks.truthy(opts, `${fnTag} arg options`); @@ -52,18 +91,6 @@ export class PluginObjectStoreIpfs implements IPluginObjectStore { Checks.nonBlankString(opts.parentDir, `${fnTag} options.parentDir`); Checks.truthy(opts.ipfsClientOrOptions, `${fnTag} ipfsClientOrOptions`); - if (isIpfsHttpClientOptions(opts.ipfsClientOrOptions)) { - this.ipfs = opts.ipfsClientOrOptions; - } else if (opts.ipfsClientOrOptions) { - this.ipfs = create({ - ...(this.opts.ipfsClientOrOptions as Options), - }); - } else { - const errorMessage = `${fnTag} Need either "ipfsClient" or "ipfsClientOptions" to construct ${this.className} Neither was provided.`; - throw new RuntimeError(errorMessage); - } - Checks.truthy(this.ipfs, `${fnTag} arg options.backend`); - const level = this.opts.logLevel || "INFO"; const label = this.className; this.log = LoggerProvider.getOrCreate({ level, label }); @@ -79,7 +106,7 @@ export class PluginObjectStoreIpfs implements IPluginObjectStore { } public async onPluginInit(): Promise { - return; // no-op + return this.initIpfs(); } public async registerWebServices( @@ -130,7 +157,8 @@ export class PluginObjectStoreIpfs implements IPluginObjectStore { public async get(req: GetObjectRequestV1): Promise { const keyPath = this.getKeyPath(req); - const chunksIterable = this.ipfs.files.read(keyPath); + const ipfs = await this.getIpfs(); + const chunksIterable = ipfs.files.read(keyPath); const chunks = []; for await (const chunk of chunksIterable) { chunks.push(chunk); @@ -151,7 +179,8 @@ export class PluginObjectStoreIpfs implements IPluginObjectStore { const checkedAt = new Date().toJSON(); const keyPath = this.getKeyPath(req); try { - const statResult = await this.ipfs.files.stat(keyPath); + const ipfs = await this.getIpfs(); + const statResult = await ipfs.files.stat(keyPath); this.log.debug(`StatResult for ${req.key}: %o`, statResult); return { key: req.key, checkedAt, isPresent: true }; } catch (ex) { @@ -170,7 +199,8 @@ export class PluginObjectStoreIpfs implements IPluginObjectStore { try { this.log.debug(`Seting object ${keyPath} in IPFS...`); const buffer = Buffer.from(req.value, "base64"); - await this.ipfs.files.write(keyPath, buffer, { + const ipfs = await this.getIpfs(); + await ipfs.files.write(keyPath, buffer, { create: true, parents: true, }); diff --git a/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/public-api.ts b/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/public-api.ts index 46e031206d..3a2fd6d45c 100755 --- a/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/public-api.ts +++ b/extensions/cactus-plugin-object-store-ipfs/src/main/typescript/public-api.ts @@ -1,5 +1,9 @@ export * from "./generated/openapi/typescript-axios/index"; -export { IIpfsHttpClient } from "./i-ipfs-http-client"; +export { + Options, + LikeIpfsHttpClientFile, + LikeIpfsHttpClient, +} from "./kubo-rpc-client-types"; import { IPluginFactoryOptions } from "@hyperledger/cactus-core-api"; export { PluginObjectStoreIpfs, diff --git a/extensions/cactus-plugin-object-store-ipfs/src/test/typescript/fixtures/mock/ipfs/ipfs-files-api-mock.ts b/extensions/cactus-plugin-object-store-ipfs/src/test/typescript/fixtures/mock/ipfs/ipfs-files-api-mock.ts deleted file mode 100644 index e0245e4a25..0000000000 --- a/extensions/cactus-plugin-object-store-ipfs/src/test/typescript/fixtures/mock/ipfs/ipfs-files-api-mock.ts +++ /dev/null @@ -1,157 +0,0 @@ -// Needed to disable the no-unused-vars check here because the file is full -// of unused method parameters given how this is a mock which doesn't do -// anything for the most part and that's by design. -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { CID } from "multiformats/cid"; -import { - API as FilesAPI, - ChmodOptions, - CpOptions, - MFSEntry, - MkdirOptions, - MvOptions, - ReadOptions, - RmOptions, - StatOptions, - StatResult, - TouchOptions, - WriteOptions, -} from "ipfs-core-types/src/files"; - -import { AbortOptions } from "ipfs-core-types"; -import { IPFSPath } from "ipfs-core-types/src/utils"; -import { RuntimeError } from "run-time-error"; - -import { Logger, Checks, LogLevelDesc } from "@hyperledger/cactus-common"; -import { LoggerProvider } from "@hyperledger/cactus-common"; -import { K_IPFS_JS_HTTP_ERROR_FILE_DOES_NOT_EXIST } from "../../../../../main/typescript/plugin-object-store-ipfs"; - -export interface IFilesApiMockOptions { - logLevel?: LogLevelDesc; -} - -export class FilesApiMock implements FilesAPI { - public static readonly CLASS_NAME = "FilesApiMock"; - - private readonly log: Logger; - private readonly data: Map; - - public get className(): string { - return FilesApiMock.CLASS_NAME; - } - - constructor(public readonly options: IFilesApiMockOptions) { - const fnTag = `${this.className}#constructor()`; - Checks.truthy(options, `${fnTag} arg options`); - - this.data = new Map(); - - const level = this.options.logLevel || "INFO"; - const label = this.className; - this.log = LoggerProvider.getOrCreate({ level, label }); - this.log.debug(`Instantiated ${this.className} OK`); - } - - public async chmod( - path: string, - mode: string | number, - options?: ChmodOptions | undefined, - ): Promise { - throw new RuntimeError("Method chmod() not implemented"); - } - - public async cp( - from: IPFSPath | IPFSPath[], - to: string, - options?: CpOptions | undefined, - ): Promise { - throw new RuntimeError("Method cp() not implemented"); - } - - public async mkdir( - path: string, - options?: MkdirOptions | undefined, - ): Promise { - throw new RuntimeError("Method mkdir() not implemented"); - } - - public async stat( - ipfsPath: IPFSPath, - options?: StatOptions | undefined, - ): Promise { - if (typeof ipfsPath !== "string") { - throw new RuntimeError("Sorry, the mock only supports string IPFS paths"); - } - if (this.data.has(ipfsPath)) { - return {} as StatResult; - } else { - throw new RuntimeError(K_IPFS_JS_HTTP_ERROR_FILE_DOES_NOT_EXIST); - } - } - - public async touch( - ipfsPath: string, - options?: TouchOptions | undefined, - ): Promise { - throw new RuntimeError("Method touch() not implemented"); - } - - public async rm( - ipfsPaths: string | string[], - options?: RmOptions | undefined, - ): Promise { - throw new RuntimeError("Method rm() not implemented"); - } - - public async *read( - ipfsPath: IPFSPath, - options?: ReadOptions | undefined, - ): AsyncIterable { - if (typeof ipfsPath !== "string") { - throw new RuntimeError("Sorry, the mock only supports string IPFS paths"); - } - const buffer = this.data.get(ipfsPath); - if (!buffer) { - throw new RuntimeError(K_IPFS_JS_HTTP_ERROR_FILE_DOES_NOT_EXIST); - } - yield buffer; - } - - public async write( - ipfsPath: string, - content: - | string - | Uint8Array - | AsyncIterable - | Blob - | Iterable, - options?: WriteOptions | undefined, - ): Promise { - if (!(content instanceof Buffer)) { - throw new RuntimeError("Sorry, this mock only supports Buffer content."); - } - this.data.set(ipfsPath, content); - } - - public async mv( - from: string | string[], - to: string, - options?: MvOptions | undefined, - ): Promise { - throw new RuntimeError("Method mv() not implemented"); - } - - public async flush( - ipfsPath: string, - options?: AbortOptions | undefined, - ): Promise { - throw new RuntimeError("Method flush() not implemented"); - } - - public ls( - ipfsPath: IPFSPath, - options?: AbortOptions | undefined, - ): AsyncIterable { - throw new RuntimeError("Method ls() not implemented"); - } -} diff --git a/extensions/cactus-plugin-object-store-ipfs/src/test/typescript/integration/plugin-object-store-ipfs.test.ts b/extensions/cactus-plugin-object-store-ipfs/src/test/typescript/integration/plugin-object-store-ipfs.test.ts index 4cfe3d2ee2..2e53341eb6 100644 --- a/extensions/cactus-plugin-object-store-ipfs/src/test/typescript/integration/plugin-object-store-ipfs.test.ts +++ b/extensions/cactus-plugin-object-store-ipfs/src/test/typescript/integration/plugin-object-store-ipfs.test.ts @@ -1,21 +1,24 @@ import test, { Test } from "tape-promise/tape"; -import { create } from "ipfs-http-client"; import express from "express"; import bodyParser from "body-parser"; import http from "http"; import type { AddressInfo } from "net"; - import { v4 as uuidv4 } from "uuid"; -import { IListenOptions, Servers } from "@hyperledger/cactus-common"; -import type { LogLevelDesc } from "@hyperledger/cactus-common"; +import { + IListenOptions, + Servers, + LogLevelDesc, +} from "@hyperledger/cactus-common"; import { Configuration } from "@hyperledger/cactus-core-api"; -import { GoIpfsTestContainer } from "@hyperledger/cactus-test-tooling"; -import { Containers } from "@hyperledger/cactus-test-tooling"; +import { + GoIpfsTestContainer, + Containers, +} from "@hyperledger/cactus-test-tooling"; +import { dynamicImportKuboRpcClientESMWorkaround } from "@hyperledger/cacti-esm-compat-hacks"; import { PluginObjectStoreIpfs } from "../../../main/typescript"; - import { DefaultApi as ObjectStoreIpfsApi } from "../../../main/typescript/public-api"; const logLevel: LogLevelDesc = "TRACE"; @@ -58,7 +61,8 @@ test(testCase, async (t: Test) => { t.comment(`Go IPFS Test Container API URL: ${ipfsApiUrl}`); t.comment(`Go IPFS Test Container Gateway URL: ${ipfsGatewayUrl}`); - const ipfsClientOrOptions = create({ + const kuboRpcModule = await dynamicImportKuboRpcClientESMWorkaround(); + const ipfsClientOrOptions = kuboRpcModule.create({ url: ipfsApiUrl, }); const instanceId = uuidv4(); diff --git a/extensions/cactus-plugin-object-store-ipfs/src/test/typescript/unit/plugin-object-store-ipfs.test.ts b/extensions/cactus-plugin-object-store-ipfs/src/test/typescript/unit/plugin-object-store-ipfs.test.ts index fd5ce1eee6..e804c9fbad 100644 --- a/extensions/cactus-plugin-object-store-ipfs/src/test/typescript/unit/plugin-object-store-ipfs.test.ts +++ b/extensions/cactus-plugin-object-store-ipfs/src/test/typescript/unit/plugin-object-store-ipfs.test.ts @@ -1,24 +1,23 @@ import type { AddressInfo } from "net"; import http from "http"; import test, { Test } from "tape-promise/tape"; - import { v4 as uuidv4 } from "uuid"; -import { create } from "ipfs-http-client"; import express from "express"; import bodyParser from "body-parser"; import { Servers } from "@hyperledger/cactus-common"; import type { IListenOptions, LogLevelDesc } from "@hyperledger/cactus-common"; import { Configuration } from "@hyperledger/cactus-core-api"; +import { dynamicImportKuboRpcClientESMWorkaround } from "@hyperledger/cacti-esm-compat-hacks"; import { PluginObjectStoreIpfs } from "../../../main/typescript"; import type { IPluginObjectStoreIpfsOptions } from "../../../main/typescript"; - import { DefaultApi as ObjectStoreIpfsApi } from "../../../main/typescript/public-api"; -test("PluginObjectStoreIpfs", (t1: Test) => { +test("PluginObjectStoreIpfs", async (t1: Test) => { const logLevel: LogLevelDesc = "TRACE"; - const ipfsClientOrOptions = create(); + const kuboRpcModule = await dynamicImportKuboRpcClientESMWorkaround(); + const ipfsClientOrOptions = kuboRpcModule.create(); t1.doesNotThrow( () => new PluginObjectStoreIpfs({ @@ -50,8 +49,9 @@ test("PluginObjectStoreIpfs", (t1: Test) => { }); test.skip("get,set,has,delete alters state as expected", async (t: Test) => { + const kuboRpcModule = await dynamicImportKuboRpcClientESMWorkaround(); const options: IPluginObjectStoreIpfsOptions = { - ipfsClientOrOptions: create(), // FIXME: use an actual mock IPFS client + ipfsClientOrOptions: kuboRpcModule.create(), // FIXME: use an actual mock IPFS client instanceId: uuidv4(), parentDir: "/" + uuidv4(), logLevel, diff --git a/extensions/cactus-plugin-object-store-ipfs/tsconfig.json b/extensions/cactus-plugin-object-store-ipfs/tsconfig.json index 10485206c2..3e1666ab5c 100644 --- a/extensions/cactus-plugin-object-store-ipfs/tsconfig.json +++ b/extensions/cactus-plugin-object-store-ipfs/tsconfig.json @@ -22,6 +22,9 @@ { "path": "../../packages/cactus-core-api/tsconfig.json" }, + { + "path": "../../packages/cacti-esm-compat-hacks/tsconfig.json" + }, { "path": "../../packages/cactus-test-tooling/tsconfig.json" } diff --git a/jest.config.js b/jest.config.js index ff19a579f6..3609db3ba1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,6 +8,7 @@ module.exports = { testTimeout: 60 * 60 * 1000, testMatch: [ `**/cactus-*/src/test/typescript/{unit,integration,benchmark}/**/*.test.ts`, + `**/cacti-*/src/test/typescript/{unit,integration,benchmark}/**/*.test.ts`, ], // Ignore the tests that are still using tap/tape for as their test runner testPathIgnorePatterns: [ diff --git a/packages/cacti-esm-compat-hacks/README.md b/packages/cacti-esm-compat-hacks/README.md new file mode 100644 index 0000000000..a46890a5fd --- /dev/null +++ b/packages/cacti-esm-compat-hacks/README.md @@ -0,0 +1,17 @@ +# `@hyperledger/cacti-esm-compat-hacks` + +Small library with hacks and helper functions to interoperate with ECMAScript modules + +## Usage + +### Dynamic import of kubo-rpc-client + +```typescript +import { dynamicImportKuboRpcClientESMWorkaround } from "@hyperledger/cacti-esm-compat-hacks"; + +// Must be called inside an async method +const kuboRpcModule = await dynamicImportKuboRpcClientESMWorkaround(); +const ipfsClientOrOptions = kuboRpcModule.create({ + url: ipfsApiUrl, +}); +``` diff --git a/packages/cacti-esm-compat-hacks/package.json b/packages/cacti-esm-compat-hacks/package.json new file mode 100644 index 0000000000..4621501db8 --- /dev/null +++ b/packages/cacti-esm-compat-hacks/package.json @@ -0,0 +1,64 @@ +{ + "name": "@hyperledger/cacti-esm-compat-hacks", + "version": "2.0.0-alpha.2", + "description": "Small library with hacks and helper functions to interoperate with ECMAScript modules", + "keywords": [ + "Hyperledger", + "Cactus", + "Integration", + "Blockchain", + "Distributed Ledger Technology" + ], + "homepage": "https://github.com/hyperledger/cacti#readme", + "bugs": { + "url": "https://github.com/hyperledger/cacti/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hyperledger/cacti.git" + }, + "license": "Apache-2.0", + "author": { + "name": "Hyperledger Cactus Contributors", + "email": "cactus@lists.hyperledger.org", + "url": "https://www.hyperledger.org/use/cacti" + }, + "contributors": [ + { + "name": "Please add yourself to the list of contributors", + "email": "your.name@example.com", + "url": "https://example.com" + }, + { + "name": "Michal Bajer", + "email": "michal.bajer@fujitsu.com", + "url": "https://www.fujitsu.com/global/" + } + ], + "main": "dist/lib/main/typescript/index.js", + "module": "dist/lib/main/typescript/index.js", + "browser": "dist/cacti-esm-compat-hacks.web.umd.js", + "types": "dist/lib/main/typescript/index.d.ts", + "files": [ + "dist/*" + ], + "scripts": { + "webpack": "npm-run-all webpack:dev", + "webpack:dev": "npm-run-all webpack:dev:node webpack:dev:web", + "webpack:dev:node": "webpack --env=dev --target=node --config ../../webpack.config.js", + "webpack:dev:web": "webpack --env=dev --target=web --config ../../webpack.config.js" + }, + "dependencies": { + "kubo-rpc-client": "3.0.1" + }, + "engines": { + "node": ">=16", + "npm": ">=8" + }, + "publishConfig": { + "access": "public" + }, + "browserMinified": "dist/cacti-esm-compat-hacks.web.umd.min.js", + "mainMinified": "dist/cacti-esm-compat-hacks.node.umd.min.js", + "watch": {} +} diff --git a/packages/cacti-esm-compat-hacks/src/main/typescript/dynamic-import.ts b/packages/cacti-esm-compat-hacks/src/main/typescript/dynamic-import.ts new file mode 100644 index 0000000000..fc76cb5e23 --- /dev/null +++ b/packages/cacti-esm-compat-hacks/src/main/typescript/dynamic-import.ts @@ -0,0 +1,20 @@ +/** + * Dynamically import `kubo-rpc-client` module (which is ESM-only and importing it from CJS ends up in error). + * + * @deprecated + * This is a workaround until we migrate to ESM or find another solution. + * Read more on rationale: https://github.com/hyperledger/cacti/pull/2829 + * + * @returns `kubo-rpc-client` module + */ +export async function dynamicImportKuboRpcClientESMWorkaround(): Promise< + typeof import("kubo-rpc-client") +> { + console.warn( + "WARNING - Do not use dynamicImportKuboRpcClientESMWorkaround() unless it's absolutely necessary!", + "It uses eval to fix an issue of transpiling dynamic imports to `require` statement, see https://github.com/microsoft/TypeScript/issues/43329.", + "Switch to regular import when possible (when ESM is supported).", + ); + + return eval('import("kubo-rpc-client")'); +} diff --git a/packages/cacti-esm-compat-hacks/src/main/typescript/index.ts b/packages/cacti-esm-compat-hacks/src/main/typescript/index.ts new file mode 100755 index 0000000000..87cb558397 --- /dev/null +++ b/packages/cacti-esm-compat-hacks/src/main/typescript/index.ts @@ -0,0 +1 @@ +export * from "./public-api"; diff --git a/packages/cacti-esm-compat-hacks/src/main/typescript/index.web.ts b/packages/cacti-esm-compat-hacks/src/main/typescript/index.web.ts new file mode 100755 index 0000000000..87cb558397 --- /dev/null +++ b/packages/cacti-esm-compat-hacks/src/main/typescript/index.web.ts @@ -0,0 +1 @@ +export * from "./public-api"; diff --git a/packages/cacti-esm-compat-hacks/src/main/typescript/public-api.ts b/packages/cacti-esm-compat-hacks/src/main/typescript/public-api.ts new file mode 100755 index 0000000000..49487c66d2 --- /dev/null +++ b/packages/cacti-esm-compat-hacks/src/main/typescript/public-api.ts @@ -0,0 +1 @@ +export { dynamicImportKuboRpcClientESMWorkaround } from "./dynamic-import"; diff --git a/packages/cacti-esm-compat-hacks/src/test/typescript/integration/api-surface.test.ts b/packages/cacti-esm-compat-hacks/src/test/typescript/integration/api-surface.test.ts new file mode 100644 index 0000000000..34aba3a0ae --- /dev/null +++ b/packages/cacti-esm-compat-hacks/src/test/typescript/integration/api-surface.test.ts @@ -0,0 +1,6 @@ +import * as apiSurface from "../../../main/typescript/public-api"; +import "jest-extended"; + +test("Library can be loaded", async () => { + expect(apiSurface).toBeTruthy(); +}); diff --git a/packages/cacti-esm-compat-hacks/tsconfig.json b/packages/cacti-esm-compat-hacks/tsconfig.json new file mode 100644 index 0000000000..15e612701b --- /dev/null +++ b/packages/cacti-esm-compat-hacks/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist/lib/", + "declarationDir": "dist/lib", + "rootDir": "./src", + "tsBuildInfoFile": "../../.build-cache/cacti-esm-compat-hacks.tsbuildinfo" + }, + "include": [ + "./src" + ], + "references": [] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 318ab009df..fbe2bb2070 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,9 @@ { "path": "./packages/cacti-ledger-browser/tsconfig.json" }, + { + "path": "./packages/cacti-esm-compat-hacks/tsconfig.json" + }, { "path": "./packages/cactus-common/tsconfig.json" }, diff --git a/yarn.lock b/yarn.lock index 4507415d30..296779a4a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6551,6 +6551,14 @@ __metadata: languageName: node linkType: hard +"@hyperledger/cacti-esm-compat-hacks@2.0.0-alpha.2, @hyperledger/cacti-esm-compat-hacks@workspace:packages/cacti-esm-compat-hacks": + version: 0.0.0-use.local + resolution: "@hyperledger/cacti-esm-compat-hacks@workspace:packages/cacti-esm-compat-hacks" + dependencies: + kubo-rpc-client: 3.0.1 + languageName: unknown + linkType: soft + "@hyperledger/cacti-example-cbdc-bridging-frontend@workspace:examples/cactus-example-cbdc-bridging-frontend": version: 0.0.0-use.local resolution: "@hyperledger/cacti-example-cbdc-bridging-frontend@workspace:examples/cactus-example-cbdc-bridging-frontend" @@ -8014,16 +8022,18 @@ __metadata: version: 0.0.0-use.local resolution: "@hyperledger/cactus-plugin-object-store-ipfs@workspace:extensions/cactus-plugin-object-store-ipfs" dependencies: + "@hyperledger/cacti-esm-compat-hacks": 2.0.0-alpha.2 "@hyperledger/cactus-common": 2.0.0-alpha.2 "@hyperledger/cactus-core": 2.0.0-alpha.2 "@hyperledger/cactus-core-api": 2.0.0-alpha.2 "@hyperledger/cactus-test-tooling": 2.0.0-alpha.2 + "@multiformats/multiaddr": 11.6.1 "@types/express": 4.17.19 axios: 1.5.1 express: 4.18.2 ipfs-core-types: 0.14.1 - ipfs-http-client: 60.0.1 - multiformats: 9.4.9 + ipfs-unixfs: 9.0.1 + multiformats: 11.0.2 run-time-error: 1.4.0 typescript-optional: 2.0.1 uuid: 8.3.2 @@ -9649,6 +9659,23 @@ __metadata: languageName: node linkType: hard +"@libp2p/crypto@npm:^1.0.11": + version: 1.0.17 + resolution: "@libp2p/crypto@npm:1.0.17" + dependencies: + "@libp2p/interface-keys": ^1.0.2 + "@libp2p/interfaces": ^3.2.0 + "@noble/ed25519": ^1.6.0 + "@noble/secp256k1": ^1.5.4 + multiformats: ^11.0.0 + node-forge: ^1.1.0 + protons-runtime: ^5.0.0 + uint8arraylist: ^2.4.3 + uint8arrays: ^4.0.2 + checksum: 178474409ffe56ba6fb6b0f691e0b5de7fafb61c18a1a1197d75d0f9471e614c67d77fce84e337238d0835ba4b7bbc7f4b72ff9447968706c2ba190ed93cf650 + languageName: node + linkType: hard + "@libp2p/interface-connection@npm:^4.0.0": version: 4.0.0 resolution: "@libp2p/interface-connection@npm:4.0.0" @@ -9672,6 +9699,13 @@ __metadata: languageName: node linkType: hard +"@libp2p/interface-keys@npm:^1.0.2": + version: 1.0.8 + resolution: "@libp2p/interface-keys@npm:1.0.8" + checksum: 08c2976b3436b6e4e6159d1c817bb9e41af41d159cb94afaa8c63cf0fbf8842b0e5ca758d6c38453a59d8e4e87cec3117e3c574d316b94fed8db842a77f6efb0 + languageName: node + linkType: hard + "@libp2p/interface-peer-id@npm:^2.0.0, @libp2p/interface-peer-id@npm:^2.0.2": version: 2.0.2 resolution: "@libp2p/interface-peer-id@npm:2.0.2" @@ -10005,7 +10039,7 @@ __metadata: languageName: node linkType: hard -"@multiformats/multiaddr@npm:^11.1.5": +"@multiformats/multiaddr@npm:11.6.1, @multiformats/multiaddr@npm:^11.1.5": version: 11.6.1 resolution: "@multiformats/multiaddr@npm:11.6.1" dependencies: @@ -10128,6 +10162,13 @@ __metadata: languageName: node linkType: hard +"@noble/ed25519@npm:^1.6.0": + version: 1.7.3 + resolution: "@noble/ed25519@npm:1.7.3" + checksum: 45169927d51de513e47bbeebff3a603433c4ac7579e1b8c5034c380a0afedbe85e6959be3d69584a7a5ed6828d638f8f28879003b9bb2fb5f22d8aa2d88fd5fe + languageName: node + linkType: hard + "@noble/hashes@npm:1.1.2": version: 1.1.2 resolution: "@noble/hashes@npm:1.1.2" @@ -10149,7 +10190,7 @@ __metadata: languageName: node linkType: hard -"@noble/secp256k1@npm:1.7.1, @noble/secp256k1@npm:~1.7.0": +"@noble/secp256k1@npm:1.7.1, @noble/secp256k1@npm:^1.5.4, @noble/secp256k1@npm:~1.7.0": version: 1.7.1 resolution: "@noble/secp256k1@npm:1.7.1" checksum: d2301f1f7690368d8409a3152450458f27e54df47e3f917292de3de82c298770890c2de7c967d237eff9c95b70af485389a9695f73eb05a43e2bd562d18b18cb @@ -15161,7 +15202,7 @@ __metadata: languageName: node linkType: hard -"any-signal@npm:^3.0.0": +"any-signal@npm:^3.0.0, any-signal@npm:^3.0.1": version: 3.0.1 resolution: "any-signal@npm:3.0.1" checksum: 073eb14c365b7552f9f16fbf36cd76171e4a0fe156a8faa865fe1d5ac4ed2f5c5ab6e3faad0ac0d4c69511b5892971c5573baa8a1cbf85fe250d0c54ff0734ff @@ -28510,7 +28551,7 @@ __metadata: languageName: node linkType: hard -"ipfs-core-utils@npm:^0.18.1": +"ipfs-core-utils@npm:^0.18.0, ipfs-core-utils@npm:^0.18.1": version: 0.18.1 resolution: "ipfs-core-utils@npm:0.18.1" dependencies: @@ -28565,7 +28606,7 @@ __metadata: languageName: node linkType: hard -"ipfs-unixfs@npm:^9.0.0": +"ipfs-unixfs@npm:9.0.1, ipfs-unixfs@npm:^9.0.0": version: 9.0.1 resolution: "ipfs-unixfs@npm:9.0.1" dependencies: @@ -28575,7 +28616,7 @@ __metadata: languageName: node linkType: hard -"ipfs-utils@npm:^9.0.13": +"ipfs-utils@npm:^9.0.13, ipfs-utils@npm:^9.0.7": version: 9.0.14 resolution: "ipfs-utils@npm:9.0.14" dependencies: @@ -32020,6 +32061,33 @@ __metadata: languageName: node linkType: hard +"kubo-rpc-client@npm:3.0.1": + version: 3.0.1 + resolution: "kubo-rpc-client@npm:3.0.1" + dependencies: + "@ipld/dag-cbor": ^9.0.0 + "@ipld/dag-json": ^10.0.0 + "@ipld/dag-pb": ^4.0.0 + "@libp2p/crypto": ^1.0.11 + "@libp2p/logger": ^2.0.5 + "@libp2p/peer-id": ^2.0.0 + "@multiformats/multiaddr": ^11.1.5 + any-signal: ^3.0.1 + dag-jose: ^4.0.0 + err-code: ^3.0.1 + ipfs-core-utils: ^0.18.0 + ipfs-utils: ^9.0.7 + it-first: ^2.0.0 + it-last: ^2.0.0 + merge-options: ^3.0.4 + multiformats: ^11.0.0 + parse-duration: ^1.0.2 + stream-to-it: ^0.2.4 + uint8arrays: ^4.0.3 + checksum: 19de983eccf131c7a53f7c73bc8446ccd49436d32d6bb215f93fe2084fbff196d3d722bee11c22c3c48531a2d71c7521241a299b08fa9302907a3a0478fb51a3 + languageName: node + linkType: hard + "kuler@npm:^2.0.0": version: 2.0.0 resolution: "kuler@npm:2.0.0" @@ -34635,14 +34703,7 @@ __metadata: languageName: node linkType: hard -"multiformats@npm:9.4.9": - version: 9.4.9 - resolution: "multiformats@npm:9.4.9" - checksum: 93dec933ef5849cab1e1cf4374f08f1943905d609cce16eb4ada2742163e5d89254fec65a380a9e2e3968affa349020ffdbf91a9813d3583c45630fa9bea71c3 - languageName: node - linkType: hard - -"multiformats@npm:^11.0.0, multiformats@npm:^11.0.2": +"multiformats@npm:11.0.2, multiformats@npm:^11.0.0, multiformats@npm:^11.0.2": version: 11.0.2 resolution: "multiformats@npm:11.0.2" checksum: e587bbe709f29e42ae3c22458c960070269027d962183afc49a83b8ba26c31525e81ce2ac71082a52ba0a75e9aed4d0d044cac68d32656fdcd5cd340fb367fac @@ -37073,6 +37134,13 @@ __metadata: languageName: node linkType: hard +"parse-duration@npm:^1.0.2": + version: 1.1.0 + resolution: "parse-duration@npm:1.1.0" + checksum: 3cfc10aa61b3a06373a347289e1704de47d5d845c79330bbab20b54c02567f3710ba84544a3a44a986c3381c68670d89542fe9de607fb0814e52f78b34893cd9 + languageName: node + linkType: hard + "parse-glob@npm:^3.0.4": version: 3.0.4 resolution: "parse-glob@npm:3.0.4" @@ -39422,6 +39490,16 @@ __metadata: languageName: node linkType: hard +"protons-runtime@npm:^5.0.0": + version: 5.0.5 + resolution: "protons-runtime@npm:5.0.5" + dependencies: + uint8arraylist: ^2.4.3 + uint8arrays: ^4.0.6 + checksum: 3709164d2cde019a065b0a1a5872e70eeb188c71c45f699b86f4aa389d2f4282a46022998370131afa0e04173fbb56feec903e81fd3e8021ae8ddcba73f87ded + languageName: node + linkType: hard + "proxy-addr@npm:~2.0.5, proxy-addr@npm:~2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" @@ -43674,7 +43752,7 @@ __metadata: languageName: node linkType: hard -"stream-to-it@npm:^0.2.2": +"stream-to-it@npm:^0.2.2, stream-to-it@npm:^0.2.4": version: 0.2.4 resolution: "stream-to-it@npm:0.2.4" dependencies: @@ -46321,7 +46399,7 @@ __metadata: languageName: node linkType: hard -"uint8arrays@npm:^4.0.2": +"uint8arrays@npm:^4.0.2, uint8arrays@npm:^4.0.3, uint8arrays@npm:^4.0.6": version: 4.0.6 resolution: "uint8arrays@npm:4.0.6" dependencies: