diff --git a/package.json b/package.json index 87dd1e4..2d43ac4 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "stdlib:pack": "node scripts/pack-stdlib-to-object.js", "wasm:dist": "cp src/tolkfiftlib.js dist && cp src/tolkfiftlib.wasm.js dist", "stdlib:dist": "cp -r src/tolk-stdlib dist && cp src/stdlib.tolk.js dist", - "test": "yarn wasm:pack && yarn stdlib:pack && yarn jest" + "test": "yarn wasm:pack && yarn stdlib:pack && yarn jest", + "fmt": "prettier --write -l --cache .", + "fmt:check": "prettier --check --cache ." }, "author": "TON Blockchain", "license": "MIT", @@ -22,14 +24,38 @@ "url": "git+https://github.com/ton-blockchain/tolk-js.git" }, "devDependencies": { - "@ton/core": "^0.56.3", + "@ton/core": "^0.61.0", "@ton/crypto": "^3.3.0", "@types/jest": "^29.5.12", "jest": "^29.7.0", "ts-jest": "^29.2.4", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "prettier": "^3.6.2" }, "dependencies": { - "arg": "^5.0.2" + "arg": "^5.0.2", + "ton-source-map": "0.2.1", + "ton-assembly": "0.3.1" + }, + "prettier": { + "arrowParens": "avoid", + "bracketSpacing": false, + "printWidth": 100, + "semi": false, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false, + "quoteProps": "preserve", + "overrides": [ + { + "files": [ + "*.yaml", + "*.yml" + ], + "options": { + "tabWidth": 2 + } + } + ] } } diff --git a/src/cli.ts b/src/cli.ts index 6e89a97..e435cdb 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -13,6 +13,7 @@ async function tolkJsCli() { '--output-fift': String, '--experimental-options': String, '--cwd': String, + '--source-map': Boolean, '-v': '--version', '-h': '--help', @@ -31,6 +32,7 @@ Options: --output-fif - output .fif file with Fift code output --experimental-options - set experimental compiler options, comma-separated --cwd , -C — sets cwd to locate .tolk files (doesn't affect output paths) +--source-map — collect a source map for debugging `) process.exit(0) } @@ -57,6 +59,7 @@ Options: entrypointFileName: args._[0], experimentalOptions: args['--experimental-options'], fsReadCallback: p => fs.readFileSync(cwd ? path.join(cwd, p) : p, 'utf-8'), + collectSourceMap: args['--source-map'] === true, }) if (result.status === 'error') { @@ -71,6 +74,10 @@ Options: codeBoc64: result.codeBoc64, codeHashHex: result.codeHashHex, sourcesSnapshot: result.sourcesSnapshot, + fiftSourceMapCode: result.fiftSourceMapCode, + sourceMapCodeRecompiledBoc64: result.sourceMapCodeRecompiledBoc64, + sourceMapCodeBoc64: result.sourceMapCodeBoc64, + sourceMap: result.sourceMap, }, null, 2)) } diff --git a/src/index.ts b/src/index.ts index 2f05180..db97fc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,8 @@ import wasmBase64 from "./tolkfiftlib.wasm.js" // @ts-ignore import stdlibContents from "./stdlib.tolk.js" import {realpath} from "./path-utils" +import {Cell, runtime, text, trace} from "ton-assembly"; +import {AssemblyMapping, HighLevelMapping, SourceMap} from "ton-source-map" let wasmBinary: Uint8Array | undefined = undefined @@ -19,6 +21,7 @@ export type TolkCompilerConfig = { withStackComments?: boolean withSrcLineComments?: boolean experimentalOptions?: string + collectSourceMap?: boolean } export type TolkResultSuccess = { @@ -27,9 +30,24 @@ export type TolkResultSuccess = { codeBoc64: string codeHashHex: string stderr: string + fiftSourceMapCode?: string + sourceMapCodeRecompiledBoc64?: string + sourceMapCodeBoc64?: string + sourceMap?: SourceMap sourcesSnapshot: { filename: string, contents: string }[] } +type TolkCompilerResultSuccess = { + status: "ok" + fiftCode: string + codeBoc64: string + codeHashHex: string + stderr: string + fiftSourceMapCode?: string + sourceMapCodeBoc64?: string + sourceMap?: HighLevelMapping +} + export type TolkResultError = { status: "error" message: string @@ -122,6 +140,7 @@ export async function runTolkCompiler(compilerConfig: TolkCompilerConfig): Promi withStackComments: compilerConfig.withStackComments, withSrcLineComments: compilerConfig.withSrcLineComments, experimentalOptions: compilerConfig.experimentalOptions, + collectSourceMap: compilerConfig.collectSourceMap, }) const configStrPtr = copyToCStringAllocating(mod, configStr) @@ -129,10 +148,75 @@ export async function runTolkCompiler(compilerConfig: TolkCompilerConfig): Promi const resultPtr = mod._tolk_compile(configStrPtr, callbackPtr) allocatedPointers.push(resultPtr) - const result: TolkResultSuccess | TolkResultError = JSON.parse(copyFromCString(mod, resultPtr)) + const result: TolkCompilerResultSuccess | TolkResultError = JSON.parse(copyFromCString(mod, resultPtr)) allocatedPointers.forEach(ptr => mod._free(ptr)) mod.removeFunction(callbackPtr) - return result.status === 'error' ? result : {...result, sourcesSnapshot} + if (result.status === 'error') { + return result + } + + if (compilerConfig.collectSourceMap) { + // When we compile with a source map enabled, the compiler generates special DEBUGMARK %id + // instructions that describe the start of a code section with a specific ID. + // These instructions, along with the rest of the Fifth code, are compiled into "poisoned" + // bitcode. + // The result of this compilation is stored in the `sourceMapCodeBoc64` field. + // + // The code generated in this way is not runnable, since the DEBUGMARK instruction is + // unknown to TVM, running such code directly will cause TVM to crash. + // + // And this is where the further code comes into play. + // + // Its task is to disassemble bitcode back into instructions, including DEBUGMARK, and + // compile it back into bitcode. + // Thanks to DEBUGMARK instructions, upon recompilation, TASM can map of each instruction + // and the debug section, thus getting a complete source code map that is accurately down + // to the specific TVM instruction. + + const sourceMapCodeCell = Cell.fromBase64(result.sourceMapCodeBoc64 ?? result.codeBoc64) + const [cleanCell, mapping] = recompileCell(sourceMapCodeCell); + const assemblyMapping: AssemblyMapping = trace.createMappingInfo(mapping) + + if (result.sourceMap === undefined) { + console.warn('Source map was not generated. This is probably a bug in Tolk compiler.') + } + + return { + ...result, + codeBoc64: result.codeBoc64, + sourceMapCodeRecompiledBoc64: cleanCell.toBoc().toString('base64'), + sourceMapCodeBoc64: result.sourceMapCodeBoc64, + sourceMap: { + highlevelMapping: result.sourceMap ?? emptyHighlevelMapping, + assemblyMapping, + recompiledCode: cleanCell.toBoc().toString('base64'), + }, + sourcesSnapshot, + } + } + + return {...result, sourcesSnapshot, sourceMap: undefined} +} + +function recompileCell(cell: Cell): [Cell, runtime.Mapping] { + const instructions = runtime.decompileCell(cell); + const assembly = text.print(instructions); + + const parseResult = text.parse("out.tasm", assembly); + if (parseResult.$ === "ParseFailure") { + throw new Error("Cannot parse resulting text Assembly"); + } + + return runtime.compileCellWithMapping(parseResult.instructions, {skipRefs: true}); } + +const emptyHighlevelMapping: HighLevelMapping = { + version: "0", + language: "tolk", + compiler_version: "", + files: [], + globals: [], + locations: [], +}; diff --git a/tests/__snapshots__/source-maps.spec.ts.snap b/tests/__snapshots__/source-maps.spec.ts.snap new file mode 100644 index 0000000..df227f5 --- /dev/null +++ b/tests/__snapshots__/source-maps.spec.ts.snap @@ -0,0 +1,386 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`source-maps should generate correct source map for lazy match 1`] = ` +" + 1 | struct (0x1) Single { + 2 | x: int32 + 3 | } + 4 | + 5 | struct (0x2) Pair { + 6 | x: int32 + 7 | y: int32 + 8 | } + 9 | + 10 | type Data = Single | Pair + 11 | + 12 | fun <#3>main(data: slice) { + 13 | <#4>val thing = <#9>lazy Data<#5>.fromSlice(data); + 14 | + 15 | match (thing) { + 16 | <#10>Single => { + 17 | <#11>throw thing.x; + 18 | } + 19 | <#12>Pair => { + 20 | <#14>throw thing.x <#13>+ thing.y; + 21 | } + 22 | else => { + 23 | <#16>return <#15>10; + 24 | } + 25 | } + 26 | } + +Cell 1821d664...: + 0: SETCP + 16: DICTPUSHCONST + 40: DICTIGETJMPZ + 56: THROWARG + 56: RET + +Cell 259edd3f...: + 0: SDBEGINSQ [#3,#4,#5,#6,#7,#8,#9,#10] + 32: PUSHCONT_SHORT + 40: PLDI + 64: THROWANY [#11] + 64: RET + 80: IFJMP + 88: SDBEGINSQ [#12] + 120: PUSHCONT + 136: LDI + 152: PLDI + 176: ADD [#13] + 184: THROWANY [#14] + 184: RET + 200: IFJMP + 208: DROP [#15] + 216: PUSHINT_4 + 216: RET [#16] + +" +`; + +exports[`source-maps should generate correct source map for simple function 1`] = ` +" + 1 | fun <#3>main(a: int) { + 2 | <#4>var x = a <#6>+ <#5>1; + 3 | <#9>return <#8>max(x, <#7>10); + 4 | } + +Cell 808c59db...: + 0: SETCP + 16: DICTPUSHCONST + 40: DICTIGETJMPZ + 56: THROWARG + 56: RET + +Cell 71e0ec90...: + 0: INC [#3,#4,#5,#6] + 8: PUSHINT_4 [#7] + 16: MAX [#8] + 16: RET [#9] + +" +`; + +exports[`source-maps should generate source map 1`] = ` +{ + "assemblyMapping": { + "cells": { + "8bea7604d0b072b209c4e479a40999439f4f67f574f4a47691381dda9451d308": { + "instructions": [ + { + "debugSections": [ + 3, + 4, + ], + "loc": { + "file": "out.tasm", + "line": 5, + "otherLines": [], + }, + "name": "PUSHINT_4", + "offset": 0, + }, + { + "debugSections": [ + 5, + ], + "loc": { + "file": "out.tasm", + "line": 7, + "otherLines": [], + }, + "name": "MAX", + "offset": 8, + }, + { + "debugSections": [ + 6, + ], + "loc": { + "file": "out.tasm", + "line": 7, + "otherLines": [], + }, + "name": "RET", + "offset": 8, + }, + ], + }, + "fd8692835670899ef97044b0a2d78da8c72156229c1f48102feed7753ceadef0": { + "instructions": [ + { + "debugSections": [], + "loc": { + "file": "out.tasm", + "line": 0, + "otherLines": [], + }, + "name": "SETCP", + "offset": 0, + }, + { + "debugSections": [], + "loc": { + "file": "out.tasm", + "line": 1, + "otherLines": [], + }, + "name": "DICTPUSHCONST", + "offset": 16, + }, + { + "debugSections": [], + "loc": { + "file": "out.tasm", + "line": 11, + "otherLines": [], + }, + "name": "DICTIGETJMPZ", + "offset": 40, + }, + { + "debugSections": [], + "loc": { + "file": "out.tasm", + "line": 12, + "otherLines": [], + }, + "name": "THROWARG", + "offset": 56, + }, + { + "debugSections": [], + "loc": { + "file": "out.tasm", + "line": 12, + "otherLines": [], + }, + "name": "RET", + "offset": 56, + }, + ], + }, + }, + "dictionaryCells": [ + { + "cell": "a9e0df6bc8c41cca2abd6f8bbc8de4da6a520b743348e40b468cce4d251a674a", + "dataCell": "8bea7604d0b072b209c4e479a40999439f4f67f574f4a47691381dda9451d308", + "offset": 8, + }, + ], + }, + "highlevelMapping": { + "compiler_version": "1.1.0", + "files": [], + "globals": [], + "language": "tolk", + "locations": [ + { + "context": { + "containing_function": "MapLookupResult.loadValue", + "description": { + "ast_kind": "ast_function_declaration", + }, + "event": "EnterFunction", + "inlining": { + "containing_func_inline_mode": 0, + }, + }, + "debug": undefined, + "idx": 0, + "loc": { + "column": 26, + "end_column": 0, + "end_line": 0, + "file": "@stdlib/common.tolk", + "length": 1, + "line": 926, + }, + "variables": [], + }, + { + "context": { + "containing_function": "MapLookupResult.loadValue", + "description": { + "ast_kind": "ast_not_null_operator", + }, + "inlining": { + "containing_func_inline_mode": 0, + }, + }, + "debug": undefined, + "idx": 1, + "loc": { + "column": 14, + "end_column": 0, + "end_line": 0, + "file": "@stdlib/common.tolk", + "length": 1, + "line": 927, + }, + "variables": [], + }, + { + "context": { + "containing_function": "MapLookupResult.loadValue", + "description": { + "ast_kind": "ast_return_statement", + }, + "event": "LeaveFunction", + "inlining": { + "containing_func_inline_mode": 0, + }, + }, + "debug": undefined, + "idx": 2, + "loc": { + "column": 3, + "end_column": 0, + "end_line": 0, + "file": "@stdlib/common.tolk", + "length": 1, + "line": 927, + }, + "variables": [], + }, + { + "context": { + "containing_function": "main", + "description": { + "ast_kind": "ast_function_declaration", + }, + "event": "EnterFunction", + "inlining": { + "containing_func_inline_mode": 0, + }, + }, + "debug": undefined, + "idx": 3, + "loc": { + "column": 3, + "end_column": 0, + "end_line": 0, + "file": "wallet-code.tolk", + "length": 1, + "line": 0, + }, + "variables": [ + { + "name": "a", + "type": "int", + }, + ], + }, + { + "context": { + "containing_function": "main", + "description": { + "ast_kind": "ast_int_const", + }, + "inlining": { + "containing_func_inline_mode": 0, + }, + }, + "debug": undefined, + "idx": 4, + "loc": { + "column": 17, + "end_column": 0, + "end_line": 0, + "file": "wallet-code.tolk", + "length": 1, + "line": 1, + }, + "variables": [ + { + "name": "a", + "type": "int", + }, + ], + }, + { + "context": { + "containing_function": "main", + "description": { + "ast_kind": "ast_function_call", + }, + "inlining": { + "containing_func_inline_mode": 0, + }, + }, + "debug": undefined, + "idx": 5, + "loc": { + "column": 10, + "end_column": 0, + "end_line": 0, + "file": "wallet-code.tolk", + "length": 1, + "line": 1, + }, + "variables": [ + { + "name": "a", + "type": "int", + }, + { + "constant_value": "10", + "name": "'1", + "type": "int", + }, + ], + }, + { + "context": { + "containing_function": "main", + "description": { + "ast_kind": "ast_return_statement", + }, + "event": "LeaveFunction", + "inlining": { + "containing_func_inline_mode": 0, + }, + }, + "debug": undefined, + "idx": 6, + "loc": { + "column": 3, + "end_column": 0, + "end_line": 0, + "file": "wallet-code.tolk", + "length": 1, + "line": 1, + }, + "variables": [ + { + "name": "'2", + "type": "int", + }, + ], + }, + ], + "version": "1.0.0", + }, + "recompiledCode": "te6cckEBAgEAEwABFP8A9KQT9LzyyAsBAAjTerYJGThbJA==", +} +`; diff --git a/tests/source-maps.spec.ts b/tests/source-maps.spec.ts new file mode 100644 index 0000000..4cb9c52 --- /dev/null +++ b/tests/source-maps.spec.ts @@ -0,0 +1,142 @@ +import {runTolkCompiler} from "../src"; +import {HighLevelSourceMapEntry, SourceMap} from "ton-source-map" + +describe('source-maps', () => { + it('should generate source map', async () => { + const file = `fun main(a: int) { + return max(a, 10); +}` + + const result = await runTolkCompiler({ + entrypointFileName: "wallet-code.tolk", + fsReadCallback: () => file, + collectSourceMap: true, + }) + if (result.status !== 'ok') { + throw result.message + } + + if (result.sourceMap?.highlevelMapping) { + const sourceMap = { + ...result.sourceMap, + highlevelMapping: { + ...result.sourceMap.highlevelMapping, + files: [], + locations: result.sourceMap.highlevelMapping.locations.map(it => ({ + ...it, + debug: undefined + })) + } + } + expect(sourceMap).toMatchSnapshot() + } + }); + + it('should generate correct source map for simple function', async () => { + const file = `fun main(a: int) { + var x = a + 1; + return max(x, 10); +}` + await doTest(file); + }); + + it('should generate correct source map for lazy match', async () => { + const file = `struct (0x1) Single { + x: int32 +} + +struct (0x2) Pair { + x: int32 + y: int32 +} + +type Data = Single | Pair + +fun main(data: slice) { + val thing = lazy Data.fromSlice(data); + + match (thing) { + Single => { + throw thing.x; + } + Pair => { + throw thing.x + thing.y; + } + else => { + return 10; + } + } +}` + await doTest(file); + }); + + + async function doTest(file: string) { + const result = await runTolkCompiler({ + entrypointFileName: "test.tolk", + fsReadCallback: () => file, + collectSourceMap: true, + withStackComments: true, + }) + if (result.status !== 'ok') { + throw result.message + } + + expect(result.sourceMap).toBeDefined() + const visualization = visualizeMappings(result.sourceMap!, file) + expect(visualization).toMatchSnapshot() + } +}) + +function visualizeMappings(sourceMap: SourceMap, sourceCode: string): string { + const {highlevelMapping, assemblyMapping} = sourceMap + let result = '' + + result += "\n" + + const lines = sourceCode.split('\n') + const locationsByLine = new Map() + + for (const location of highlevelMapping.locations) { + if (!locationsByLine.has(location.loc.line)) { + locationsByLine.set(location.loc.line, []) + } + locationsByLine.get(location.loc.line)!.push(location) + } + + for (let i = 0; i < lines.length; i++) { + const lineNum = i + const line = lines[i] + const lineLocations = locationsByLine.get(lineNum) || [] + + lineLocations.sort((a, b) => a.loc.column - b.loc.column) + + let modifiedLine = line + let offset = 0 + + for (const location of lineLocations) { + const marker = `<#${location.idx}>` + const insertPos = location.loc.column + 1 + offset + modifiedLine = modifiedLine.slice(0, insertPos) + marker + modifiedLine.slice(insertPos) + offset += marker.length + } + + result += `${(lineNum + 1).toString().padStart(3)} | ${modifiedLine}\n` + } + + result += "\n" + + for (const [cellHash, cell] of Object.entries(assemblyMapping.cells)) { + if (!cell) continue + result += `Cell ${cellHash.slice(0, 8)}...:\n` + for (const instruction of cell.instructions) { + const debugStr = instruction.debugSections.length > 0 + ? ` [${instruction.debugSections.map(it => `#${it}`).join(',')}]` + : '' + result += ` ${instruction.offset.toString().padStart(3)}: ${instruction.name}${debugStr}\n` + } + result += '\n' + } + + return result +} diff --git a/tsconfig.json b/tsconfig.json index 26feb07..6d83659 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,4 +12,4 @@ "include": [ "src/**/*" ] -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index d8316e3..50e3c2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -529,10 +529,10 @@ dependencies: "@sinonjs/commons" "^3.0.0" -"@ton/core@^0.56.3": - version "0.56.3" - resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.56.3.tgz#1162764573abb76032eba70f8497e5cb2ea532ee" - integrity sha512-HVkalfqw8zqLLPehtq0CNhu5KjVzc7IrbDwDHPjGoOSXmnqSobiWj8a5F+YuWnZnEbQKtrnMGNOOjVw4LG37rg== +"@ton/core@^0.61.0": + version "0.61.0" + resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.61.0.tgz#09b37801cb2f5a942020fcc992be1e99f4b16689" + integrity sha512-0qyVfP2dDue2bq80ydXggo2MlufcmzuFk6G94qRrZxvyQ3NSe4UeBTeRf1gQmN7tywgTsX2gS61e4yvJrlUu4Q== dependencies: symbol.inspect "1.0.1" @@ -552,6 +552,11 @@ jssha "3.2.0" tweetnacl "1.0.3" +"@tonstudio/parser-runtime@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@tonstudio/parser-runtime/-/parser-runtime-0.0.1.tgz#469955fb7ea354d4fadaa5964359b11fd17f926b" + integrity sha512-5s4fLkXWxa4SAd7QGGvJXe13GakEo0J3VF5dUI/i3A//bGZxMwCp1FcnbErpNs3y0LcAZoXE5FCUnDowDQptqw== + "@types/babel__core@^7.1.14": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -815,6 +820,11 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1893,6 +1903,11 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +prettier@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393" + integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== + pretty-format@^29.0.0, pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" @@ -2109,6 +2124,19 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +ton-assembly@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/ton-assembly/-/ton-assembly-0.3.1.tgz#be5d9c75ed36e4297b5956e72da5f9c15fff8468" + integrity sha512-AEbjnN4Osy+gqoNuNaY8YfUTTsyhbtr8Q8UexHL5M1FT45SzsXTer9UDakFo8tyoq3Uddfgy6d46AHUEGd8mvw== + dependencies: + "@tonstudio/parser-runtime" "^0.0.1" + cac "^6.7.14" + +ton-source-map@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ton-source-map/-/ton-source-map-0.2.1.tgz#76996e09df3f50f8a7d39398e9aab66694d1d84f" + integrity sha512-EeMfdGcHFeBOVa2pxit9RJFUzFM1KwelfxzqqUDURt2h2tcJT0TheOenbcWgpF6FZVLUkSFgysCl1dif2BJ3HA== + ts-jest@^29.2.4: version "29.2.4" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.4.tgz#38ccf487407d7a63054a72689f6f99b075e296e5"