diff --git a/examples/ccd-js-gen/index.ts b/examples/ccd-js-gen/index.ts new file mode 100644 index 000000000..36e82245c --- /dev/null +++ b/examples/ccd-js-gen/index.ts @@ -0,0 +1,58 @@ +import { credentials } from '@grpc/grpc-js'; +import * as SDK from '@concordium/node-sdk'; +import meow from 'meow'; +import { parseEndpoint } from '../shared/util'; + +// Importing the generated smart contract module client. +// eslint-disable-next-line import/no-unresolved +import * as Module from './lib/module'; + +const cli = meow( + ` + Usage + $ yarn run-example [options] + + Required + --index, -i The index of the smart contract + + Options + --help, -h Displays this message + --endpoint, -e Specify endpoint of a grpc2 interface of a Concordium node in the format "address:port". Defaults to 'localhost:20000' + --subindex, The subindex of the smart contract. Defaults to 0 +`, + { + importMeta: import.meta, + flags: { + endpoint: { + type: 'string', + alias: 'e', + default: 'localhost:20000', + }, + index: { + type: 'number', + alias: 'i', + isRequired: true, + }, + subindex: { + type: 'number', + default: 0, + }, + }, + } +); + +const [address, port] = parseEndpoint(cli.flags.endpoint); +const grpcClient = SDK.createConcordiumClient( + address, + Number(port), + credentials.createInsecure() +); + +const contractAddress: SDK.ContractAddress = { + index: BigInt(cli.flags.index), + subindex: BigInt(cli.flags.subindex), +}; + +const contract = new Module.SmartContractTestBench(grpcClient, contractAddress); + +contract.dryRun.getAccountAddress(''); diff --git a/examples/ccd-js-gen/module.wasm.v1 b/examples/ccd-js-gen/module.wasm.v1 new file mode 100644 index 000000000..4e6fcbca8 Binary files /dev/null and b/examples/ccd-js-gen/module.wasm.v1 differ diff --git a/examples/package.json b/examples/package.json index c18e8b798..425613c5d 100644 --- a/examples/package.json +++ b/examples/package.json @@ -2,6 +2,8 @@ "name": "@concordium/examples", "type": "module", "dependencies": { + "@concordium/ccd-js-gen": "workspace:^", + "@concordium/common-sdk": "workspace:^", "@concordium/node-sdk": "workspace:^", "@grpc/grpc-js": "^1.3.4", "@noble/ed25519": "^1.7.1", @@ -22,6 +24,7 @@ "lint": "eslint . --cache --ext .ts,.tsx --max-warnings 0", "lint-fix": "yarn lint --fix; exit 0", "typecheck": "tsc --noEmit", - "run-example": "node --experimental-specifier-resolution=node --loader ts-node/esm" + "run-example": "node --experimental-specifier-resolution=node --loader ts-node/esm", + "generate-module": "ccd-js-gen --module ccd-js-gen/module.wasm.v1 --out-dir ccd-js-gen/lib" } } diff --git a/package.json b/package.json index 0cad16afa..33d900797 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "./packages/common", "./packages/nodejs", "./packages/web", + "./packages/ccd-js-gen", "./examples" ] }, diff --git a/packages/ccd-js-gen/bin/ccd-js-gen.js b/packages/ccd-js-gen/bin/ccd-js-gen.js new file mode 100755 index 000000000..b330ceff4 --- /dev/null +++ b/packages/ccd-js-gen/bin/ccd-js-gen.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node +// eslint-disable-next-line @typescript-eslint/no-var-requires +const cli = require('../dist/src/cli.js'); +cli.main(); diff --git a/packages/ccd-js-gen/package.json b/packages/ccd-js-gen/package.json new file mode 100644 index 000000000..adceae06e --- /dev/null +++ b/packages/ccd-js-gen/package.json @@ -0,0 +1,50 @@ +{ + "name": "@concordium/ccd-js-gen", + "version": "1.0.0", + "description": "Generate JS clients for the Concordium Blockchain", + "bin": "bin/ccd-js-gen.js", + "main": "dist/src/lib.js", + "typings": "dist/src/lib.d.ts", + "scripts": { + "build": "tsc", + "clean": "rm -r lib", + "lint": "eslint src/** bin/** --cache --ext .ts,.js --max-warnings 0", + "lint-fix": "yarn --silent lint --fix; exit 0", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "concordium", + "smart-contracts" + ], + "repository": { + "type": "git", + "url": "https://github.com/Concordium/concordium-node-sdk-js", + "directory": "packages/ccd-js-gen" + }, + "author": { + "name": "Concordium Software", + "email": "support@concordium.software", + "url": "https://concordium.com" + }, + "license": "Apache-2.0", + "dependencies": { + "@concordium/common-sdk": "9.0.0", + "commander": "^11.0.0", + "ts-morph": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^20.5.0", + "@typescript-eslint/eslint-plugin": "^4.28.1", + "@typescript-eslint/parser": "^4.28.1", + "eslint": "^7.29.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-prettier": "^3.4.0", + "prettier": "^2.3.2", + "typescript": "^4.3.5" + }, + "prettier": { + "singleQuote": true, + "tabWidth": 4 + } +} diff --git a/packages/ccd-js-gen/src/cli.ts b/packages/ccd-js-gen/src/cli.ts new file mode 100644 index 000000000..b3b3db8d8 --- /dev/null +++ b/packages/ccd-js-gen/src/cli.ts @@ -0,0 +1,31 @@ +import { Command } from 'commander'; +import packageJson from '../package.json'; +import * as lib from './lib'; + +type Options = { + module: string; + outDir: string; +}; + +export async function main(): Promise { + const program = new Command(); + + program + .name(packageJson.name) + .description(packageJson.description) + .version(packageJson.version); + + program + .requiredOption( + '-m, --module ', + 'Smart contract module to generate clients from' + ) + .requiredOption( + '-o, --out-dir ', + 'The output directory for the generated code' + ) + .parse(process.argv); + + const options = program.opts(); + await lib.generateContractClients(options.module, options.outDir); +} diff --git a/packages/ccd-js-gen/src/lib.ts b/packages/ccd-js-gen/src/lib.ts new file mode 100644 index 000000000..5c7da133f --- /dev/null +++ b/packages/ccd-js-gen/src/lib.ts @@ -0,0 +1,217 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import * as tsm from 'ts-morph'; +import * as SDK from '@concordium/common-sdk'; + +/** + * Generate smart contract client code for a given smart contract module. + * @param modulePath Path to the smart contract module. + * @param outDirPath Path to the directory to use for the output. + */ +export async function generateContractClients( + modulePath: string, + outDirPath: string +): Promise { + // TODO catch if file does not exist and produce better error. + const fileBytes = await fs.readFile(modulePath); + const module = SDK.Module.from(fileBytes); + + const moduleInterface = await module.parseModuleInterface(); + + const outputName = path.basename(modulePath, '.wasm.v1'); + const outputFilePath = path.format({ + name: outputName, + dir: outDirPath, + ext: '.ts', + }); + + const compilerOptions: tsm.CompilerOptions = { + outDir: outDirPath, + declaration: true, + }; + const project = new tsm.Project({ compilerOptions }); + const sourceFile = project.createSourceFile(outputFilePath, '', { + overwrite: true, + }); + addModuleClients(sourceFile, moduleInterface); + await Promise.all([project.save(), project.emit()]); +} + +/** Iterates a module interface adding code to the provided source file. */ +function addModuleClients( + sourceFile: tsm.SourceFile, + moduleInterface: SDK.ModuleInterface +) { + sourceFile.addImportDeclaration({ + namespaceImport: 'SDK', + moduleSpecifier: '@concordium/common-sdk', + }); + + for (const contract of moduleInterface.values()) { + const contractNameId = 'contractName'; + const genericContractId = 'genericContract'; + const grpcClientId = 'grpcClient'; + const contractAddressId = 'contractAddress'; + const dryRunId = 'dryRun'; + const contractClassId = toPascalCase(contract.contractName); + const contractDryRunClassId = `${contractClassId}DryRun`; + + const classDecl = sourceFile.addClass({ + docs: ['Smart contract client for a contract instance on chain.'], + isExported: true, + name: contractClassId, + properties: [ + { + docs: [ + 'Name of the smart contract supported by this client.', + ], + scope: tsm.Scope.Public, + isReadonly: true, + name: contractNameId, + type: 'string', + initializer: `'${contract.contractName}'`, + }, + { + docs: ['Generic contract client used internally.'], + scope: tsm.Scope.Private, + name: genericContractId, + type: 'SDK.Contract', + }, + { + docs: ['Dry run entrypoints of the smart contract.'], + scope: tsm.Scope.Public, + name: dryRunId, + type: contractDryRunClassId, + }, + ], + }); + + const dryRunClassDecl = sourceFile.addClass({ + docs: [ + `Smart contract client for dry running messages to a contract instance of '${contract.contractName}' on chain.`, + ], + isExported: true, + name: contractDryRunClassId, + }); + + classDecl + .addConstructor({ + docs: ['Contruct a client for a contract instance on chain'], + parameters: [ + { + name: grpcClientId, + type: 'SDK.ConcordiumGRPCClient', + scope: tsm.Scope.Public, + }, + { + name: contractAddressId, + type: 'SDK.ContractAddress', + isReadonly: true, + scope: tsm.Scope.Public, + }, + ], + }) + .setBodyText( + `this.${genericContractId} = new SDK.Contract(${grpcClientId}, ${contractAddressId}, '${contract.contractName}'); +this.${dryRunId} = new ${contractDryRunClassId}(this.${genericContractId});` + ); + + dryRunClassDecl.addConstructor({ + docs: ['Contruct a client for a contract instance on chain'], + parameters: [ + { + name: genericContractId, + type: 'SDK.Contract', + scope: tsm.Scope.Private, + }, + ], + }); + + for (const entrypointName of contract.entrypointNames) { + const receiveName = `${contract.contractName}.${entrypointName}`; + const transactionMetadataId = 'transactionMetadata'; + const parameterId = 'parameter'; + const signerId = 'signer'; + classDecl + .addMethod({ + docs: [ + `Send a message to the '${entrypointName}' entrypoint of the '${contract.contractName}' contract.`, + ], + scope: tsm.Scope.Public, + name: toCamelCase(entrypointName), + parameters: [ + { + name: transactionMetadataId, + type: 'SDK.ContractTransactionMetadata', + }, + { + name: parameterId, + type: 'SDK.HexString', + }, + { + name: signerId, + type: 'SDK.AccountSigner', + }, + ], + returnType: 'Promise', + }) + .setBodyText( + `return this.${genericContractId}.createAndSendUpdateTransaction( + '${receiveName}', + SDK.encodeHexString, + ${transactionMetadataId}, + ${parameterId}, + ${signerId} +);` + ); + const blockHashId = 'blockHash'; + dryRunClassDecl + .addMethod({ + docs: [ + `Dry run a message to the '${entrypointName}' entrypoint of the '${contract.contractName}' contract`, + ], + scope: tsm.Scope.Public, + name: toCamelCase(entrypointName), + parameters: [ + { + name: parameterId, + type: 'SDK.HexString', + }, + { + name: blockHashId, + type: 'SDK.HexString', + hasQuestionToken: true, + }, + ], + returnType: 'Promise', + }) + .setBodyText( + `return this.${genericContractId}.invokeView( + '${receiveName}', + SDK.encodeHexString, + (hex: SDK.HexString) => hex, + ${parameterId}, + ${blockHashId} +);` + ); + } + } +} + +/** Make the first character in a string uppercase */ +function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.substring(1); +} + +/** Convert a string in snake_case or kebab-case into camelCase. */ +function toCamelCase(str: string): string { + return str + .split(/[-_]/g) + .map((word, index) => (index === 0 ? word : capitalize(word))) + .join(''); +} + +/** Convert a string in snake_case or kebab-case into PascalCase. */ +function toPascalCase(str: string): string { + return str.split(/[-_]/g).map(capitalize).join(''); +} diff --git a/packages/ccd-js-gen/tsconfig.eslint.json b/packages/ccd-js-gen/tsconfig.eslint.json new file mode 100644 index 000000000..b189885ac --- /dev/null +++ b/packages/ccd-js-gen/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["./src/**/*", "bin/**/*"] +} diff --git a/packages/ccd-js-gen/tsconfig.json b/packages/ccd-js-gen/tsconfig.json new file mode 100644 index 000000000..93480aa50 --- /dev/null +++ b/packages/ccd-js-gen/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig-base.json", + "include": ["src/**/*", "package.json"], + "compilerOptions": { + "outDir": "./lib", + "lib": ["ES2020", "dom"], // "dom" is only added to get the typings for the global variable `WebAssembly`. + "target": "ES2020", + "resolveJsonModule": true + } +} diff --git a/packages/common/src/contractHelpers.ts b/packages/common/src/contractHelpers.ts index 46ab71d90..6f498b864 100644 --- a/packages/common/src/contractHelpers.ts +++ b/packages/common/src/contractHelpers.ts @@ -40,3 +40,69 @@ export const isEqualContractAddress = (a: ContractAddress) => (b: ContractAddress): boolean => a.index === b.index && a.subindex === b.subindex; + +/** The name of a smart contract. Note: This does _not_ including the 'init_' prefix. */ +export type ContractName = string; + +/** The name of an entrypoint exposed by a smart contract. Note: This does _not_ include the '.' prefix. */ +export type EntrypointName = string; + +/** Check that every character is an Ascii alpha, numeric or punctuation. */ +function isAsciiAlphaNumericPunctuation(string: string) { + for (let i = 0; i < string.length; i++) { + const charCode = string.charCodeAt(i); + if ( + (32 <= charCode && charCode <= 47) || // Punctuation ! to / + (48 <= charCode && charCode <= 57) || // Numeric + (58 <= charCode && charCode <= 64) || // Punctuation : to @ + (65 <= charCode && charCode <= 90) || // Uppercase alpha + (91 <= charCode && charCode <= 96) || // Punctuation [ to ` + (97 <= charCode && charCode <= 122) || // Lowercase alpha + (123 <= charCode && charCode <= 126) // Punctuation { to ~ + ) { + continue; + } else { + return false; + } + } + return true; +} + +/** Check if a string is a valid smart contract init name. */ +export function isInitName(string: string): boolean { + return ( + string.length <= 100 && + string.startsWith('init_') && + !string.includes('.') && + isAsciiAlphaNumericPunctuation(string) + ); +} + +/** Get the contract name from a string. Assumes the string is a valid init name. */ +export function getContractNameFromInit(initName: string): ContractName { + return initName.substring(5); +} + +/** Check if a string is a valid smart contract receive name. */ +export function isReceiveName(string: string): boolean { + return ( + string.length <= 100 && + string.includes('.') && + isAsciiAlphaNumericPunctuation(string) + ); +} + +/** Get the contract name and entrypoint name from a string. Assumes the string is a valid receive name. */ +export function getNamesFromReceive(receiveName: string): { + contractName: ContractName; + entrypointName: EntrypointName; +} { + const splitPoint = receiveName.indexOf('.'); + if (splitPoint === -1) { + throw new Error('Invalid receive name'); + } + return { + contractName: receiveName.substring(0, splitPoint), + entrypointName: receiveName.substring(splitPoint + 1), + }; +} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index f1732d1ea..313cb92e6 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -15,6 +15,7 @@ export { serializeAccountTransactionPayload, serializeCredentialDeploymentPayload, } from './serialization'; +export { encodeHexString } from './serializationHelpers'; export { sha256 }; export { CredentialRegistrationId } from './types/CredentialRegistrationId'; export { AccountAddress } from './types/accountAddress'; @@ -22,6 +23,7 @@ export { CcdAmount } from './types/ccdAmount'; export { TransactionExpiry } from './types/transactionExpiry'; export { DataBlob } from './types/DataBlob'; export { ModuleReference } from './types/moduleReference'; +export * from './types/Module'; export * from './credentialDeploymentTransactions'; export { isAlias, getAlias } from './alias'; export { @@ -57,3 +59,4 @@ export * from './uleb128'; export * from './cis2'; export * from './cis0'; export * from './cis4'; +export * from './GenericContract'; diff --git a/packages/common/src/types/Module.ts b/packages/common/src/types/Module.ts new file mode 100644 index 000000000..3b6c79615 --- /dev/null +++ b/packages/common/src/types/Module.ts @@ -0,0 +1,103 @@ +import { ModuleReference } from './moduleReference'; +import * as H from '../contractHelpers'; +import { sha256 } from '../hash'; + +/** Interface of a smart contract containing the name of the contract and every entrypoint. */ +export type ContractInterface = { + /** The name of the smart contract. Note: This does _not_ including the 'init_' prefix. */ + contractName: H.ContractName; + /** A set of entrypoints exposed by the smart contract. Note: These does _not_ include the '.' prefix. */ + entrypointNames: Set; +}; + +/** Interface of a smart contract module containing the interface of every contract in the module. */ +export type ModuleInterface = Map; + +/** + * A versioned smart contract module. + */ +export class Module { + /** The version of the smart contract module. */ + public version: number; + /** Cache of the parsed WebAssembly module. */ + private wasmModule: WebAssembly.Module | undefined; + /** Cache of the module reference. */ + private moduleRef: ModuleReference | undefined; + + private constructor(public moduleSource: Buffer) { + this.version = moduleSource.readInt32LE(); + } + + /** + * Construct a smart contract module from the module source. + */ + public static from(moduleSource: Buffer): Module { + return new Module(moduleSource); + } + + /** Calculate the module reference from the module source. The module reference is cached. */ + public getModuleRef(): ModuleReference { + if (this.moduleRef !== undefined) { + return this.moduleRef; + } + const hash = sha256([this.moduleSource]); + return ModuleReference.fromBytes(hash); + } + + /** Parse the WebAssembly module in the smart contract module. The parsed module is cached. */ + public getWasmModule(): Promise { + return this.wasmModule !== undefined + ? Promise.resolve(this.wasmModule) + : WebAssembly.compile(this.moduleSource.subarray(8)); + } + + /** + * Build a module interface based on exports from the WebAssembly module. + * @returns The interface of the smart contract module. + */ + public async parseModuleInterface(): Promise { + const map = new Map(); + const wasmExports = WebAssembly.Module.exports( + await this.getWasmModule() + ); + + for (const exp of wasmExports) { + if (exp.kind !== 'function') { + continue; + } + if (H.isInitName(exp.name)) { + const contractName = H.getContractNameFromInit(exp.name); + getOrInsert(map, contractName, { + contractName: contractName, + entrypointNames: new Set(), + }); + continue; + } + if (H.isReceiveName(exp.name)) { + const parts = H.getNamesFromReceive(exp.name); + const entry = getOrInsert(map, parts.contractName, { + contractName: parts.contractName, + entrypointNames: new Set(), + }); + entry.entrypointNames.add(parts.entrypointName); + } + } + return map; + } +} + +/** + * Get a key from a map, if not present, insert a new value and return this. + * @param map The map to get or insert into. + * @param key The key to lookup or insert to. + * @param value The value to be inserted if nothing is present. + * @returns The value currently in the map or just insert into it. + */ +function getOrInsert(map: Map, key: K, value: V): V { + const current = map.get(key); + if (current !== undefined) { + return current; + } + map.set(key, value); + return value; +} diff --git a/yarn.lock b/yarn.lock index 4965d23f2..4468c7ae7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1312,7 +1312,28 @@ __metadata: languageName: node linkType: hard -"@concordium/common-sdk@9.0.0, @concordium/common-sdk@workspace:packages/common": +"@concordium/ccd-js-gen@workspace:^, @concordium/ccd-js-gen@workspace:packages/ccd-js-gen": + version: 0.0.0-use.local + resolution: "@concordium/ccd-js-gen@workspace:packages/ccd-js-gen" + dependencies: + "@concordium/common-sdk": 9.0.0 + "@types/node": ^20.5.0 + "@typescript-eslint/eslint-plugin": ^4.28.1 + "@typescript-eslint/parser": ^4.28.1 + commander: ^11.0.0 + eslint: ^7.29.0 + eslint-config-prettier: ^8.3.0 + eslint-plugin-import: ^2.23.4 + eslint-plugin-prettier: ^3.4.0 + prettier: ^2.3.2 + ts-morph: ^19.0.0 + typescript: ^4.3.5 + bin: + ccd-js-gen: bin/ccd-js-gen.js + languageName: unknown + linkType: soft + +"@concordium/common-sdk@9.0.0, @concordium/common-sdk@workspace:^, @concordium/common-sdk@workspace:packages/common": version: 0.0.0-use.local resolution: "@concordium/common-sdk@workspace:packages/common" dependencies: @@ -1354,6 +1375,8 @@ __metadata: version: 0.0.0-use.local resolution: "@concordium/examples@workspace:examples" dependencies: + "@concordium/ccd-js-gen": "workspace:^" + "@concordium/common-sdk": "workspace:^" "@concordium/node-sdk": "workspace:^" "@grpc/grpc-js": ^1.3.4 "@noble/ed25519": ^1.7.1 @@ -2178,6 +2201,18 @@ __metadata: languageName: node linkType: hard +"@ts-morph/common@npm:~0.20.0": + version: 0.20.0 + resolution: "@ts-morph/common@npm:0.20.0" + dependencies: + fast-glob: ^3.2.12 + minimatch: ^7.4.3 + mkdirp: ^2.1.6 + path-browserify: ^1.0.1 + checksum: eb02480971fbe045b4dd099d1ddb262d47d657197fefb73a4a2c89523975bfb0b23050207d49d19e853ef23bffdcb9d89a778f52f6b3385ae5bcf63322523700 + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.9 resolution: "@tsconfig/node10@npm:1.0.9" @@ -2383,6 +2418,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.5.0": + version: 20.5.3 + resolution: "@types/node@npm:20.5.3" + checksum: fe67a0fd7402218bdf91523a2b1c2e41d619f7294b1a471e0a778b8bc7bb3fcf291aed12041bcbe9622d50a3d1295a9adea0e7e19bb9386a246bf66071404721 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.1": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -3616,6 +3658,13 @@ __metadata: languageName: node linkType: hard +"code-block-writer@npm:^12.0.0": + version: 12.0.0 + resolution: "code-block-writer@npm:12.0.0" + checksum: 9f6505a4d668c9131c6f3f686359079439e66d5f50c236614d52fcfa53aeb0bc615b2c6c64ef05b5511e3b0433ccfd9f7756ad40eb3b9298af6a7d791ab1981d + languageName: node + linkType: hard + "collect-v8-coverage@npm:^1.0.0": version: 1.0.1 resolution: "collect-v8-coverage@npm:1.0.1" @@ -3687,6 +3736,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^11.0.0": + version: 11.0.0 + resolution: "commander@npm:11.0.0" + checksum: 6621954e1e1d078b4991c1f5bbd9439ad37aa7768d6ab4842de1dbd4d222c8a27e1b8e62108b3a92988614af45031d5bb2a2aaa92951f4d0c934d1a1ac564bb4 + languageName: node + linkType: hard + "commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -4632,6 +4688,19 @@ __metadata: languageName: node linkType: hard +"fast-glob@npm:^3.2.12": + version: 3.3.1 + resolution: "fast-glob@npm:3.3.1" + dependencies: + "@nodelib/fs.stat": ^2.0.2 + "@nodelib/fs.walk": ^1.2.3 + glob-parent: ^5.1.2 + merge2: ^1.3.0 + micromatch: ^4.0.4 + checksum: b6f3add6403e02cf3a798bfbb1183d0f6da2afd368f27456010c0bc1f9640aea308243d4cb2c0ab142f618276e65ecb8be1661d7c62a7b4e5ba774b9ce5432e5 + languageName: node + linkType: hard + "fast-glob@npm:^3.2.9": version: 3.2.11 resolution: "fast-glob@npm:3.2.11" @@ -7049,7 +7118,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^7.1.3": +"minimatch@npm:^7.1.3, minimatch@npm:^7.4.3": version: 7.4.6 resolution: "minimatch@npm:7.4.6" dependencies: @@ -7178,6 +7247,15 @@ __metadata: languageName: node linkType: hard +"mkdirp@npm:^2.1.6": + version: 2.1.6 + resolution: "mkdirp@npm:2.1.6" + bin: + mkdirp: dist/cjs/src/bin.js + checksum: 8a1d09ffac585e55f41c54f445051f5bc33a7de99b952bb04c576cafdf1a67bb4bae8cb93736f7da6838771fbf75bc630430a3a59e1252047d2278690bd150ee + languageName: node + linkType: hard + "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -7636,6 +7714,13 @@ __metadata: languageName: node linkType: hard +"path-browserify@npm:^1.0.1": + version: 1.0.1 + resolution: "path-browserify@npm:1.0.1" + checksum: c6d7fa376423fe35b95b2d67990060c3ee304fc815ff0a2dc1c6c3cfaff2bd0d572ee67e18f19d0ea3bbe32e8add2a05021132ac40509416459fffee35200699 + languageName: node + linkType: hard + "path-exists@npm:^3.0.0": version: 3.0.0 resolution: "path-exists@npm:3.0.0" @@ -9035,6 +9120,16 @@ __metadata: languageName: node linkType: hard +"ts-morph@npm:^19.0.0": + version: 19.0.0 + resolution: "ts-morph@npm:19.0.0" + dependencies: + "@ts-morph/common": ~0.20.0 + code-block-writer: ^12.0.0 + checksum: c2546da8dcbdfd5f987ef39f30e52de5cc89391b7357ad45e7a09d05d2fd0cabb92c9d1cf14860ba27e9e3476707b854ba9671a1c8e0a925b6457305ef3e23ea + languageName: node + linkType: hard + "ts-node@npm:10.9": version: 10.9.1 resolution: "ts-node@npm:10.9.1"