diff --git a/.changeset/kind-eggs-mate.md b/.changeset/kind-eggs-mate.md new file mode 100644 index 0000000000..76c00b70f7 --- /dev/null +++ b/.changeset/kind-eggs-mate.md @@ -0,0 +1,15 @@ +--- +"@evmts/bundler": patch +--- + +Updated @evmts/bundler to take a fileAccessObject as a parameter + +### Context +@evmts/bundler is the internal bundler for all other bundlers and the language server. We changed it to take fileAccessObject as a parameter instead of using `fs` and `fs/promises` + +### Impact +By taking in a file-access-object instead of using `fs` we can implement important features. + +- the ability to use virtual files in the typescript lsp before the user saves the file. +- the ability to use more peformant bun file read methods + diff --git a/.changeset/lucky-jobs-yell.md b/.changeset/lucky-jobs-yell.md new file mode 100644 index 0000000000..609715f5c0 --- /dev/null +++ b/.changeset/lucky-jobs-yell.md @@ -0,0 +1,10 @@ +--- +"@evmts/ts-plugin": patch +--- + +Updated @evmts/ts-plugin to use LSP to get files + +Previously EVMts relied on `fs.readFileSync` to implement the LSP. By replacing this with using `typescriptLanguageServer.readFile` we are able to rely on the LSP to get the file instead of the file system + +In future versions of EVMts when we add a vscode plugin this will make the LSP smart enough to update before the user even clicks `save` + diff --git a/.changeset/moody-pets-wink.md b/.changeset/moody-pets-wink.md new file mode 100644 index 0000000000..66ac3894e3 --- /dev/null +++ b/.changeset/moody-pets-wink.md @@ -0,0 +1,6 @@ +--- +"@evmts/bun-plugin": patch +--- + +Updated Bun to use native Bun.file api which is more peformant than using `fs` + diff --git a/bundlers/bun/src/bunFile.ts b/bundlers/bun/src/bunFile.ts new file mode 100644 index 0000000000..2f60e4baeb --- /dev/null +++ b/bundlers/bun/src/bunFile.ts @@ -0,0 +1 @@ +export { file } from 'bun' diff --git a/bundlers/bun/src/bunFileAccessObject.spec.ts b/bundlers/bun/src/bunFileAccessObject.spec.ts new file mode 100644 index 0000000000..41f4d061a8 --- /dev/null +++ b/bundlers/bun/src/bunFileAccessObject.spec.ts @@ -0,0 +1,98 @@ +import { file } from './bunFile' +import { bunFileAccesObject } from './bunFileAccessObject' +import * as fsPromises from 'fs/promises' +import { join } from 'path' +import { type Mock, beforeEach, describe, expect, it } from 'vitest' +import { vi } from 'vitest' + +const licensePath = join(__dirname, '../LICENSE') + +vi.mock('./bunFile', () => ({ + file: vi.fn(), +})) + +const mockFile = file as Mock + +describe('bunFileAccessObject', () => { + beforeEach(() => { + mockFile.mockImplementation((filePath: string) => ({ + exists: () => true, + text: () => fsPromises.readFile(filePath, 'utf8'), + })) + }) + describe(bunFileAccesObject.readFileSync.name, () => { + it('reads a file', () => { + const result = bunFileAccesObject.readFileSync(licensePath, 'utf8') + expect(result).toMatchInlineSnapshot(` + "(The MIT License) + + Copyright 2020-2022 + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + \\"Software\\"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED \\"AS IS\\", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + " + `) + }) + }) + + describe(bunFileAccesObject.exists.name, () => { + it('returns true if a file exists', async () => { + const result = await bunFileAccesObject.exists(licensePath) + expect(result).toBe(true) + }) + }) + + describe(bunFileAccesObject.readFile.name, () => { + it('reads a file', async () => { + const result = await bunFileAccesObject.readFile(licensePath, 'utf8') + expect(result).toMatchInlineSnapshot(` + "(The MIT License) + + Copyright 2020-2022 + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + \\"Software\\"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED \\"AS IS\\", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + " + `) + }) + }) + + describe(bunFileAccesObject.existsSync.name, () => { + it('returns true if a file exists', () => { + const result = bunFileAccesObject.existsSync(licensePath) + expect(result).toBe(true) + }) + }) +}) diff --git a/bundlers/bun/src/bunFileAccessObject.ts b/bundlers/bun/src/bunFileAccessObject.ts new file mode 100644 index 0000000000..031341f5a8 --- /dev/null +++ b/bundlers/bun/src/bunFileAccessObject.ts @@ -0,0 +1,18 @@ +import { file } from './bunFile' +import type { FileAccessObject } from '@evmts/bundler' +import { existsSync, readFileSync } from 'fs' + +export const bunFileAccesObject: FileAccessObject & { + exists: (filePath: string) => Promise +} = { + existsSync, + exists: (filePath: string) => { + const bunFile = file(filePath) + return bunFile.exists() + }, + readFile: (filePath: string) => { + const bunFile = file(filePath) + return bunFile.text() + }, + readFileSync, +} diff --git a/bundlers/bun/src/index.spec.ts b/bundlers/bun/src/index.spec.ts index 7fb54cd2f1..fcdd002a31 100644 --- a/bundlers/bun/src/index.spec.ts +++ b/bundlers/bun/src/index.spec.ts @@ -1,7 +1,8 @@ import { evmtsBunPlugin } from '.' +import { file } from './bunFile' import { bundler } from '@evmts/bundler' import { loadConfig } from '@evmts/config' -import { exists } from 'fs/promises' +import { exists, readFile } from 'fs/promises' import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@evmts/config', async () => ({ @@ -19,6 +20,12 @@ vi.mock('fs/promises', async () => ({ readFile: vi.fn().mockReturnValue('export const ExampleContract = {abi: {}}'), })) +vi.mock('./bunFile', () => ({ + file: vi.fn(), +})) + +const mockFile = file as Mock + const mockExists = exists as Mock const mockBundler = bundler as Mock @@ -37,6 +44,10 @@ const contractPath = '../../../examples/bun/ExampleContract.sol' describe('evmtsBunPlugin', () => { beforeEach(() => { + mockFile.mockImplementation((filePath: string) => ({ + exists: () => exists(filePath), + text: () => readFile(filePath, 'utf8'), + })) mockLoadConfig.mockReturnValue({}) mockBundler.mockReturnValue({ resolveEsmModule: vi.fn().mockReturnValue({ diff --git a/bundlers/bun/src/index.ts b/bundlers/bun/src/index.ts index 7e9a6c3bea..3489b2b4c5 100644 --- a/bundlers/bun/src/index.ts +++ b/bundlers/bun/src/index.ts @@ -1,7 +1,7 @@ +import { bunFileAccesObject } from './bunFileAccessObject' import { bundler } from '@evmts/bundler' import { loadConfig } from '@evmts/config' import type { BunPlugin } from 'bun' -import { exists, readFile } from 'fs/promises' type EvmtsBunPluginOptions = {} @@ -9,7 +9,7 @@ type EvmtsBunPlugin = (options?: EvmtsBunPluginOptions) => BunPlugin export const evmtsBunPlugin: EvmtsBunPlugin = () => { const config = loadConfig(process.cwd()) - const moduleResolver = bundler(config, console) + const moduleResolver = bundler(config, console, bunFileAccesObject) return { name: '@evmts/esbuild-plugin', setup(build) { @@ -42,19 +42,21 @@ export const evmtsBunPlugin: EvmtsBunPlugin = () => { build.onLoad({ filter: /\.sol$/ }, async ({ path }) => { const filePaths = { dts: `${path}.d.ts`, ts: `${path}.ts` } const [dtsExists, tsExists] = await Promise.all( - Object.values(filePaths).map((filePath) => exists(filePath)), + Object.values(filePaths).map((filePath) => + bunFileAccesObject.exists(filePath), + ), ) if (dtsExists) { const filePath = `${path}.d.ts` return { - contents: await readFile(filePath, 'utf8'), + contents: await bunFileAccesObject.readFile(filePath, 'utf8'), watchFiles: [filePath], } } if (tsExists) { const filePath = `${path}.ts` return { - contents: await readFile(filePath, 'utf8'), + contents: await bunFileAccesObject.readFile(filePath, 'utf8'), watchFiles: [filePath], } } diff --git a/bundlers/bun/tsup.config.ts b/bundlers/bun/tsup.config.ts index 626a9e4d78..3a9935a215 100644 --- a/bundlers/bun/tsup.config.ts +++ b/bundlers/bun/tsup.config.ts @@ -9,4 +9,5 @@ export default defineConfig({ splitting: false, sourcemap: true, clean: true, + bundle: false, }) diff --git a/bundlers/bundler/src/bundler.spec.ts b/bundlers/bundler/src/bundler.spec.ts index a21e9f6e27..88c5b6150c 100644 --- a/bundlers/bundler/src/bundler.spec.ts +++ b/bundlers/bundler/src/bundler.spec.ts @@ -1,7 +1,7 @@ import { bundler } from './bundler' import { resolveArtifacts, resolveArtifactsSync } from './solc' import type { SolcInputDescription, SolcOutput } from './solc/solc' -import type { Bundler, ModuleInfo } from './types' +import type { Bundler, FileAccessObject, ModuleInfo } from './types' import { writeFileSync } from 'fs' import type { Node } from 'solidity-ast/node' import * as ts from 'typescript' @@ -15,6 +15,12 @@ import { vi, } from 'vitest' +const fao: FileAccessObject = { + existsSync: vi.fn() as any, + readFile: vi.fn() as any, + readFileSync: vi.fn() as any, +} + const erc20Abi = [ { constant: true, @@ -275,7 +281,7 @@ describe(bundler.name, () => { }, } - resolver = bundler(config as any, logger) + resolver = bundler(config as any, logger, fao) vi.mock('./solc', () => { return { resolveArtifacts: vi.fn(), diff --git a/bundlers/bundler/src/bundler.ts b/bundlers/bundler/src/bundler.ts index 1122e9328b..485f9433fc 100644 --- a/bundlers/bundler/src/bundler.ts +++ b/bundlers/bundler/src/bundler.ts @@ -7,13 +7,20 @@ import type { Bundler } from './types' // @ts-ignore import solc from 'solc' -export const bundler: Bundler = (config, logger) => { +export const bundler: Bundler = (config, logger, fao) => { return { name: bundler.name, config, resolveDts: async (modulePath, basedir, includeAst) => { const { solcInput, solcOutput, artifacts, modules, asts } = - await resolveArtifacts(modulePath, basedir, logger, config, includeAst) + await resolveArtifacts( + modulePath, + basedir, + logger, + config, + includeAst, + fao, + ) if (artifacts) { const evmtsImports = `import { EvmtsContract } from '@evmts/core'` const evmtsBody = generateDtsBody(artifacts, config) @@ -29,7 +36,14 @@ export const bundler: Bundler = (config, logger) => { }, resolveDtsSync: (modulePath, basedir, includeAst) => { const { artifacts, modules, asts, solcInput, solcOutput } = - resolveArtifactsSync(modulePath, basedir, logger, config, includeAst) + resolveArtifactsSync( + modulePath, + basedir, + logger, + config, + includeAst, + fao, + ) if (artifacts) { const evmtsImports = `import { EvmtsContract } from '@evmts/core'` @@ -46,37 +60,79 @@ export const bundler: Bundler = (config, logger) => { }, resolveTsModuleSync: (modulePath, basedir, includeAst) => { const { solcInput, solcOutput, asts, artifacts, modules } = - resolveArtifactsSync(modulePath, basedir, logger, config, includeAst) + resolveArtifactsSync( + modulePath, + basedir, + logger, + config, + includeAst, + fao, + ) const code = generateRuntimeSync(artifacts, config, 'ts', logger) return { code, modules, solcInput, solcOutput, asts } }, resolveTsModule: async (modulePath, basedir, includeAst) => { const { solcInput, solcOutput, asts, artifacts, modules } = - await resolveArtifacts(modulePath, basedir, logger, config, includeAst) + await resolveArtifacts( + modulePath, + basedir, + logger, + config, + includeAst, + fao, + ) const code = await generateRuntime(artifacts, config, 'ts', logger) return { code, modules, solcInput, solcOutput, asts } }, resolveCjsModuleSync: (modulePath, basedir, includeAst) => { const { solcInput, solcOutput, asts, artifacts, modules } = - resolveArtifactsSync(modulePath, basedir, logger, config, includeAst) + resolveArtifactsSync( + modulePath, + basedir, + logger, + config, + includeAst, + fao, + ) const code = generateRuntimeSync(artifacts, config, 'cjs', logger) return { code, modules, solcInput, solcOutput, asts } }, resolveCjsModule: async (modulePath, basedir, includeAst) => { const { solcInput, solcOutput, asts, artifacts, modules } = - await resolveArtifacts(modulePath, basedir, logger, config, includeAst) + await resolveArtifacts( + modulePath, + basedir, + logger, + config, + includeAst, + fao, + ) const code = await generateRuntime(artifacts, config, 'cjs', logger) return { code, modules, solcInput, solcOutput, asts } }, resolveEsmModuleSync: (modulePath, basedir, includeAst) => { const { solcInput, solcOutput, asts, artifacts, modules } = - resolveArtifactsSync(modulePath, basedir, logger, config, includeAst) + resolveArtifactsSync( + modulePath, + basedir, + logger, + config, + includeAst, + fao, + ) const code = generateRuntimeSync(artifacts, config, 'mjs', logger) return { code, modules, solcInput, solcOutput, asts } }, resolveEsmModule: async (modulePath, basedir, includeAst) => { const { solcInput, solcOutput, asts, artifacts, modules } = - await resolveArtifacts(modulePath, basedir, logger, config, includeAst) + await resolveArtifacts( + modulePath, + basedir, + logger, + config, + includeAst, + fao, + ) const code = await generateRuntime(artifacts, config, 'mjs', logger) return { code, modules, solcInput, solcOutput, asts } }, diff --git a/bundlers/bundler/src/solc/compileContracts.spec.ts b/bundlers/bundler/src/solc/compileContracts.spec.ts index 740445b5aa..d848714a2a 100644 --- a/bundlers/bundler/src/solc/compileContracts.spec.ts +++ b/bundlers/bundler/src/solc/compileContracts.spec.ts @@ -1,8 +1,7 @@ -import type { ModuleInfo } from '../types' +import type { FileAccessObject, ModuleInfo } from '../types' import { compileContractSync } from './compileContracts' import { moduleFactory } from './moduleFactory' import type { ResolvedConfig } from '@evmts/config' -import { readFileSync } from 'fs' import * as resolve from 'resolve' // TODO wrap this in a typesafe version // @ts-ignore @@ -18,7 +17,6 @@ import { } from 'vitest' // Mock the necessary functions and modules -vi.mock('fs', () => ({ readFileSync: vi.fn() })) vi.mock('resolve', () => ({ sync: vi.fn() })) vi.mock('solc', () => { const defaultExport = { compile: vi.fn() } @@ -35,6 +33,12 @@ const ConsoleMock = { vi.stubGlobal('console', ConsoleMock) +const fao: FileAccessObject = { + readFileSync: vi.fn() as any, + existsSync: vi.fn() as any, + readFile: vi.fn() as any, +} + describe('compileContractSync', () => { const filePath = 'test/path' const basedir = 'base/dir' @@ -68,7 +72,7 @@ describe('compileContractSync', () => { Test: { abi: [], evm: { bytecode: { object: '0x123' } } }, } - const mockReadFileSync = readFileSync as Mock + const mockReadFileSync = fao.readFileSync as Mock const mockResolveSync = resolve.sync as Mock const mockModuleFactory = moduleFactory as Mock const mockSolcCompile = solc.compile as Mock @@ -93,6 +97,7 @@ describe('compileContractSync', () => { basedir, config, true, + fao, ) expect(compiledContract).toMatchInlineSnapshot(` @@ -185,13 +190,14 @@ describe('compileContractSync', () => { }, } `) - expect(readFileSync).toBeCalledWith(filePath, 'utf8') + expect(fao.readFileSync).toBeCalledWith(filePath, 'utf8') expect(resolve.sync).toBeCalledWith(filePath, { basedir }) expect(moduleFactory).toBeCalledWith( filePath, mockSource, config.remappings, config.libs, + fao, ) expect((solc.compile as Mock).mock.lastCall).toMatchInlineSnapshot(` [ @@ -206,6 +212,7 @@ describe('compileContractSync', () => { basedir, config, false, + fao, ) expect(compiledContract).toMatchInlineSnapshot(` @@ -293,13 +300,14 @@ describe('compileContractSync', () => { }, } `) - expect(readFileSync).toBeCalledWith(filePath, 'utf8') + expect(fao.readFileSync).toBeCalledWith(filePath, 'utf8') expect(resolve.sync).toBeCalledWith(filePath, { basedir }) expect(moduleFactory).toBeCalledWith( filePath, mockSource, config.remappings, config.libs, + fao, ) expect((solc.compile as Mock).mock.lastCall).toMatchInlineSnapshot(` [ @@ -316,7 +324,7 @@ describe('compileContractSync', () => { }), ) expect(() => - compileContractSync(filePath, basedir, config, false), + compileContractSync(filePath, basedir, config, false, fao), ).toThrowErrorMatchingInlineSnapshot('"Compilation failed"') expect(console.error).toHaveBeenCalledWith('Compilation errors:', [ { type: 'Error', message: 'Compilation Error' }, @@ -330,7 +338,7 @@ describe('compileContractSync', () => { errors: [{ type: 'Warning', message: 'Compilation Warning' }], }), ) - compileContractSync(filePath, basedir, config, false) + compileContractSync(filePath, basedir, config, false, fao) expect((console.warn as Mock).mock.lastCall[0]).toMatchInlineSnapshot( '"Compilation warnings:"', ) @@ -343,7 +351,7 @@ describe('compileContractSync', () => { errors: [], }), ) - compileContractSync(filePath, basedir, config, false) + compileContractSync(filePath, basedir, config, false, fao) expect(console.warn).not.toHaveBeenCalled() }) @@ -375,7 +383,7 @@ describe('compileContractSync', () => { mockModuleA.resolutions.push(mockModuleB) mockModuleFactory.mockReturnValue(mockModuleA) expect( - compileContractSync(filePath, basedir, config, false), + compileContractSync(filePath, basedir, config, false, fao), ).toMatchInlineSnapshot(` { "artifacts": undefined, diff --git a/bundlers/bundler/src/solc/compileContracts.ts b/bundlers/bundler/src/solc/compileContracts.ts index 6ba2cf2493..6787f6088d 100644 --- a/bundlers/bundler/src/solc/compileContracts.ts +++ b/bundlers/bundler/src/solc/compileContracts.ts @@ -1,9 +1,8 @@ -import type { ModuleInfo } from '../types' +import type { FileAccessObject, ModuleInfo } from '../types' import { invariant } from '../utils/invariant' import { moduleFactory } from './moduleFactory' import { type SolcInputDescription, type SolcOutput, solcCompile } from './solc' import type { ResolvedConfig } from '@evmts/config' -import { readFileSync } from 'fs' import * as resolve from 'resolve' import type { Node } from 'solidity-ast/node' @@ -13,6 +12,7 @@ export const compileContractSync = ( basedir: string, config: ResolvedConfig['compiler'], includeAst: TIncludeAsts, + fao: FileAccessObject, ): { artifacts: SolcOutput['contracts'][string] | undefined modules: Record<'string', ModuleInfo> @@ -22,7 +22,7 @@ export const compileContractSync = ( } => { const entryModule = moduleFactory( filePath, - readFileSync( + fao.readFileSync( resolve.sync(filePath, { basedir, }), @@ -30,6 +30,7 @@ export const compileContractSync = ( ), config.remappings, config.libs, + fao, ) const modules: Record = {} diff --git a/bundlers/bundler/src/solc/moduleFactory.spec.ts b/bundlers/bundler/src/solc/moduleFactory.spec.ts index 4e2d6509df..8e75e4538c 100644 --- a/bundlers/bundler/src/solc/moduleFactory.spec.ts +++ b/bundlers/bundler/src/solc/moduleFactory.spec.ts @@ -1,6 +1,5 @@ -import type { ModuleInfo } from '../types' +import type { FileAccessObject, ModuleInfo } from '../types' import { moduleFactory } from './moduleFactory' -import { readFileSync } from 'fs' import { type Mock, afterEach, @@ -11,10 +10,16 @@ import { vi, } from 'vitest' +const fao: FileAccessObject = { + existsSync: vi.fn(), + readFile: vi.fn(), + readFileSync: vi.fn(), +} + vi.mock('fs', () => ({ readFileSync: vi.fn(), })) -const mockReadFileSync = readFileSync as Mock +const mockReadFileSync = fao.readFileSync as Mock describe('moduleFactory', () => { const remappings = { @@ -42,9 +47,13 @@ import "otherOthermodule"` .mockReturnValueOnce(key1MockContent) .mockReturnValueOnce(othermoduleMockContent) - testModule = moduleFactory(absolutePath, testModuleCode, remappings, [ - '../node_modules', - ]) + testModule = moduleFactory( + absolutePath, + testModuleCode, + remappings, + ['../node_modules'], + fao, + ) }) it('should correctly resolve import paths', () => { @@ -96,6 +105,7 @@ import "otherOthermodule"` testModuleCodeUnresolvedImport, remappings, ['../node_modules'], + fao, ) // Update the expected snapshot to reflect the change. diff --git a/bundlers/bundler/src/solc/moduleFactory.ts b/bundlers/bundler/src/solc/moduleFactory.ts index 615406924d..d231f89f3b 100644 --- a/bundlers/bundler/src/solc/moduleFactory.ts +++ b/bundlers/bundler/src/solc/moduleFactory.ts @@ -1,8 +1,7 @@ -import type { ModuleInfo } from '../types' +import type { FileAccessObject, ModuleInfo } from '../types' import { invariant } from '../utils/invariant' import { resolveImportPath } from './resolveImportPath' import { resolveImports } from './resolveImports' -import { readFileSync } from 'fs' /** * Creates a module from the given module information. @@ -20,6 +19,7 @@ export const moduleFactory = ( rawCode: string, remappings: Record, libs: string[], + fao: FileAccessObject, ): ModuleInfo => { const stack = [{ absolutePath, rawCode }] const modules = new Map() @@ -73,7 +73,7 @@ export const moduleFactory = ( remappings, libs, ) - const depRawCode = readFileSync(depImportAbsolutePath, 'utf8') + const depRawCode = fao.readFileSync(depImportAbsolutePath, 'utf8') stack.push({ absolutePath: depImportAbsolutePath, rawCode: depRawCode }) }) diff --git a/bundlers/bundler/src/solc/resolveArtifacts.spec.ts b/bundlers/bundler/src/solc/resolveArtifacts.spec.ts index aeba625b65..950cfddb5b 100644 --- a/bundlers/bundler/src/solc/resolveArtifacts.spec.ts +++ b/bundlers/bundler/src/solc/resolveArtifacts.spec.ts @@ -1,4 +1,4 @@ -import type { Logger, ModuleInfo } from '../types' +import type { FileAccessObject, Logger, ModuleInfo } from '../types' import { compileContractSync } from './compileContracts' import { resolveArtifacts } from './resolveArtifacts' import { type ResolvedConfig, defaultConfig } from '@evmts/config' @@ -15,6 +15,12 @@ vi.mock('./compileContracts', () => ({ compileContractSync: vi.fn(), })) +const fao: FileAccessObject = { + existsSync: vi.fn() as any, + readFile: vi.fn() as any, + readFileSync: vi.fn() as any, +} + const solFile = 'test.sol' const basedir = 'basedir' const logger: Logger = { @@ -41,7 +47,7 @@ describe('resolveArtifacts', () => { modules: {} as Record, } as any) expect( - await resolveArtifacts(solFile, basedir, logger, config, false), + await resolveArtifacts(solFile, basedir, logger, config, false, fao), ).toMatchInlineSnapshot(` { "artifacts": { diff --git a/bundlers/bundler/src/solc/resolveArtifacts.ts b/bundlers/bundler/src/solc/resolveArtifacts.ts index dde34a3d36..c65483c67f 100644 --- a/bundlers/bundler/src/solc/resolveArtifacts.ts +++ b/bundlers/bundler/src/solc/resolveArtifacts.ts @@ -1,4 +1,4 @@ -import type { Logger, ModuleInfo } from '../types' +import type { FileAccessObject, Logger, ModuleInfo } from '../types' import { resolveArtifactsSync } from './resolveArtifactsSync' import type { SolcContractOutput, @@ -18,6 +18,7 @@ export const resolveArtifacts = async ( logger: Logger, config: ResolvedConfig, includeAst: boolean, + fao: FileAccessObject, ): Promise<{ artifacts: Artifacts modules: Record<'string', ModuleInfo> @@ -25,5 +26,5 @@ export const resolveArtifacts = async ( solcInput: SolcInputDescription solcOutput: SolcOutput }> => { - return resolveArtifactsSync(solFile, basedir, logger, config, includeAst) + return resolveArtifactsSync(solFile, basedir, logger, config, includeAst, fao) } diff --git a/bundlers/bundler/src/solc/resolveArtifactsSync.spec.ts b/bundlers/bundler/src/solc/resolveArtifactsSync.spec.ts index f4f455af56..64b4ff69a6 100644 --- a/bundlers/bundler/src/solc/resolveArtifactsSync.spec.ts +++ b/bundlers/bundler/src/solc/resolveArtifactsSync.spec.ts @@ -1,4 +1,4 @@ -import type { Logger, ModuleInfo } from '../types' +import type { FileAccessObject, Logger, ModuleInfo } from '../types' import { compileContractSync } from './compileContracts' import { resolveArtifactsSync } from './resolveArtifactsSync' import { type ResolvedConfig, defaultConfig } from '@evmts/config' @@ -15,6 +15,12 @@ vi.mock('./compileContracts', () => ({ compileContractSync: vi.fn(), })) +const fao: FileAccessObject = { + existsSync: vi.fn() as any, + readFileSync: vi.fn() as any, + readFile: vi.fn() as any, +} + const mockModules: Record = { module1: { id: 'id', @@ -58,7 +64,7 @@ const mockCompileContractSync = compileContractSync as MockedFunction< describe('resolveArtifactsSync', () => { it('should throw an error if the file is not a solidity file', () => { expect(() => - resolveArtifactsSync('test.txt', basedir, logger, config, false), + resolveArtifactsSync('test.txt', basedir, logger, config, false, fao), ).toThrowErrorMatchingInlineSnapshot('"Not a solidity file"') }) @@ -68,7 +74,7 @@ describe('resolveArtifactsSync', () => { throw new Error('Oops') }) expect(() => - resolveArtifactsSync(solFile, basedir, logger, config, false), + resolveArtifactsSync(solFile, basedir, logger, config, false, fao), ).toThrowErrorMatchingInlineSnapshot('"Oops"') }) @@ -78,7 +84,7 @@ describe('resolveArtifactsSync', () => { modules: mockModules, } as any) expect( - resolveArtifactsSync(solFile, basedir, logger, config, false), + resolveArtifactsSync(solFile, basedir, logger, config, false, fao), ).toMatchInlineSnapshot(` { "artifacts": { @@ -135,6 +141,7 @@ describe('resolveArtifactsSync', () => { logger, config, false, + fao, ) expect(artifacts).toEqual({ @@ -152,7 +159,7 @@ describe('resolveArtifactsSync', () => { }) expect(() => - resolveArtifactsSync(solFile, basedir, logger, config, false), + resolveArtifactsSync(solFile, basedir, logger, config, false, fao), ).toThrowErrorMatchingInlineSnapshot('"Compilation failed"') }) }) diff --git a/bundlers/bundler/src/solc/resolveArtifactsSync.ts b/bundlers/bundler/src/solc/resolveArtifactsSync.ts index 0b7e8bf08c..91e6062106 100644 --- a/bundlers/bundler/src/solc/resolveArtifactsSync.ts +++ b/bundlers/bundler/src/solc/resolveArtifactsSync.ts @@ -1,4 +1,4 @@ -import type { Logger, ModuleInfo } from '../types' +import type { FileAccessObject, Logger, ModuleInfo } from '../types' import { compileContractSync } from './compileContracts' import type { SolcContractOutput, @@ -19,6 +19,7 @@ export const resolveArtifactsSync = ( logger: Logger, config: ResolvedConfig, includeAst: boolean, + fao: FileAccessObject, ): { artifacts: Artifacts modules: Record<'string', ModuleInfo> @@ -30,7 +31,7 @@ export const resolveArtifactsSync = ( throw new Error('Not a solidity file') } const { artifacts, modules, asts, solcInput, solcOutput } = - compileContractSync(solFile, basedir, config.compiler, includeAst) + compileContractSync(solFile, basedir, config.compiler, includeAst, fao) if (!artifacts) { logger.error(`Compilation failed for ${solFile}`) diff --git a/bundlers/bundler/src/types.ts b/bundlers/bundler/src/types.ts index a303512aae..e30917fd82 100644 --- a/bundlers/bundler/src/types.ts +++ b/bundlers/bundler/src/types.ts @@ -2,7 +2,7 @@ import type { SolcInputDescription, SolcOutput } from './solc/solc' import type { ResolvedConfig } from '@evmts/config' import type { Node } from 'solidity-ast/node' -type BundlerResult = { +export type BundlerResult = { code: string modules: Record<'string', ModuleInfo> solcInput: SolcInputDescription @@ -10,13 +10,19 @@ type BundlerResult = { asts?: Record | undefined } -type AsyncBundlerResult = ( +export type FileAccessObject = { + readFile: (path: string, encoding: BufferEncoding) => Promise + readFileSync: (path: string, encoding: BufferEncoding) => string + existsSync: (path: string) => boolean +} + +export type AsyncBundlerResult = ( module: string, basedir: string, includeAst: boolean, ) => Promise -type SyncBundlerResult = ( +export type SyncBundlerResult = ( module: string, basedir: string, includeAst: boolean, @@ -25,6 +31,7 @@ type SyncBundlerResult = ( export type Bundler = ( config: ResolvedConfig, logger: Logger, + fao: FileAccessObject, ) => { /** * The name of the plugin. diff --git a/bundlers/bundler/src/unplugin.spec.ts b/bundlers/bundler/src/unplugin.spec.ts index d8fcea22a8..8a6f423493 100644 --- a/bundlers/bundler/src/unplugin.spec.ts +++ b/bundlers/bundler/src/unplugin.spec.ts @@ -107,6 +107,11 @@ describe('unpluginFn', () => { "trace": [Function], "warn": [Function], }, + { + "existsSync": [MockFunction spy], + "readFile": [Function], + "readFileSync": [Function], + }, ] `) diff --git a/bundlers/bundler/src/unplugin.ts b/bundlers/bundler/src/unplugin.ts index 5f45fec6d5..6c288b0a56 100644 --- a/bundlers/bundler/src/unplugin.ts +++ b/bundlers/bundler/src/unplugin.ts @@ -1,7 +1,9 @@ import * as packageJson from '../package.json' import { bundler } from './bundler' +import type { FileAccessObject } from './types' import { type ResolvedConfig, loadConfig } from '@evmts/config' -import { existsSync } from 'fs' +import { existsSync, readFileSync } from 'fs' +import { readFile } from 'fs/promises' import { createRequire } from 'module' import { type UnpluginFactory, createUnplugin } from 'unplugin' import { z } from 'zod' @@ -43,12 +45,18 @@ export const unpluginFn: UnpluginFactory< const bundler = bundlers[compilerOption] let moduleResolver: ReturnType + const fao: FileAccessObject = { + existsSync, + readFile, + readFileSync, + } + return { name: '@evmts/rollup-plugin', version: packageJson.version, async buildStart() { config = loadConfig(process.cwd()) - moduleResolver = bundler(config, console) + moduleResolver = bundler(config, console, fao) this.addWatchFile('./tsconfig.json') }, async resolveId(id, importer, options) { diff --git a/ts-plugin/src/bin/evmts-gen.ts b/ts-plugin/src/bin/evmts-gen.ts index 4de7c55eb6..56700fab37 100644 --- a/ts-plugin/src/bin/evmts-gen.ts +++ b/ts-plugin/src/bin/evmts-gen.ts @@ -1,9 +1,16 @@ -import { bundler } from '@evmts/bundler' +import { FileAccessObject, bundler } from '@evmts/bundler' import { loadConfig } from '@evmts/config' -import { writeFile } from 'fs/promises' +import { existsSync, readFileSync } from 'fs' +import { readFile, writeFile } from 'fs/promises' import { glob } from 'glob' import path from 'path' +const fao: FileAccessObject = { + existsSync: existsSync, + readFile: readFile, + readFileSync: readFileSync, +} + const generate = (cwd = process.cwd(), include = ['src/**/*.sol']) => { const files = glob.sync(include, { cwd, @@ -15,7 +22,7 @@ const generate = (cwd = process.cwd(), include = ['src/**/*.sol']) => { const fileName = file.split('/').at(-1) as string const fileDir = file.split('/').slice(0, -1).join('/') const config = loadConfig(cwd) - const plugin = bundler(config, console) + const plugin = bundler(config, console, fao) plugin .resolveTsModule(file, cwd, false) .then((dts) => diff --git a/ts-plugin/src/decorators/getDefinitionAtPosition.spec.ts b/ts-plugin/src/decorators/getDefinitionAtPosition.spec.ts index cf053edfbb..b04598698d 100644 --- a/ts-plugin/src/decorators/getDefinitionAtPosition.spec.ts +++ b/ts-plugin/src/decorators/getDefinitionAtPosition.spec.ts @@ -1,4 +1,5 @@ import { getDefinitionServiceDecorator } from './getDefinitionAtPosition' +import { FileAccessObject } from '@evmts/bundler' import typescript from 'typescript/lib/tsserverlibrary' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -23,6 +24,12 @@ vi.mock('../utils', async () => { } }) +const fao: FileAccessObject = { + existsSync: vi.fn() as any, + readFileSync: vi.fn() as any, + readFile: vi.fn() as any, +} + const mockLogger = { error: vi.fn(), } @@ -96,6 +103,7 @@ describe('getDefinitionServiceDecorator', () => { {} as any, mockLogger as any, typescript, + fao, ) const definitions = decoratedService.getDefinitionAtPosition( @@ -121,6 +129,7 @@ describe('getDefinitionServiceDecorator', () => { {} as any, mockLogger as any, typescript, + fao, ) const result = decoratedService.getDefinitionAndBoundSpan('someFile.ts', 42) @@ -156,6 +165,7 @@ describe('getDefinitionServiceDecorator', () => { {} as any, mockLogger as any, typescript, + fao, ) const definitions = decoratedService.getDefinitionAtPosition( @@ -200,6 +210,7 @@ describe('getDefinitionServiceDecorator', () => { {} as any, mockLogger as any, typescript, + fao, ) const definitions = decoratedService.getDefinitionAtPosition( @@ -235,6 +246,7 @@ describe('getDefinitionServiceDecorator', () => { {} as any, mockLogger as any, typescript, + fao, ) const definitions = decoratedService.getDefinitionAtPosition( @@ -278,6 +290,7 @@ describe('getDefinitionServiceDecorator', () => { {} as any, mockLogger as any, typescript, + fao, ) const definitions = decoratedService.getDefinitionAtPosition( diff --git a/ts-plugin/src/decorators/getDefinitionAtPosition.ts b/ts-plugin/src/decorators/getDefinitionAtPosition.ts index d84cf3292d..122a5425eb 100644 --- a/ts-plugin/src/decorators/getDefinitionAtPosition.ts +++ b/ts-plugin/src/decorators/getDefinitionAtPosition.ts @@ -4,7 +4,7 @@ import { convertSolcAstToTsDefinitionInfo, findContractDefinitionFileNameFromEvmtsNode, } from '../utils' -import { bundler } from '@evmts/bundler' +import { FileAccessObject, bundler } from '@evmts/bundler' import { ResolvedConfig } from '@evmts/config' import { Node } from 'solidity-ast/node' import { findAll } from 'solidity-ast/utils' @@ -22,6 +22,7 @@ export const getDefinitionServiceDecorator = ( config: ResolvedConfig, logger: Logger, ts: typeof typescript, + fao: FileAccessObject, ): typescript.LanguageService => { const getDefinitionAtPosition: typeof service.getDefinitionAtPosition = ( fileName, @@ -36,7 +37,7 @@ export const getDefinitionServiceDecorator = ( if (!evmtsContractPath) { return definition } - const plugin = bundler(config, logger as any) + const plugin = bundler(config, logger as any, fao) const includedAst = true const { asts, solcInput } = plugin.resolveDtsSync( evmtsContractPath, diff --git a/ts-plugin/src/decorators/getScriptKind.spec.ts b/ts-plugin/src/decorators/getScriptKind.spec.ts index 4269f3e909..431858c53d 100644 --- a/ts-plugin/src/decorators/getScriptKind.spec.ts +++ b/ts-plugin/src/decorators/getScriptKind.spec.ts @@ -1,4 +1,5 @@ import { getScriptKindDecorator } from '.' +import { FileAccessObject } from '@evmts/bundler' import { EvmtsConfig, defaultConfig, defineConfig } from '@evmts/config' import typescript from 'typescript/lib/tsserverlibrary' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -16,6 +17,12 @@ const mockConfig: EvmtsConfig = { } const config = defineConfig(() => mockConfig).configFn('.') +const fao: FileAccessObject = { + readFile: vi.fn(), + readFileSync: vi.fn(), + existsSync: vi.fn(), +} + describe(getScriptKindDecorator.name, () => { let createInfo: TestAny let logger: TestAny @@ -36,7 +43,7 @@ describe(getScriptKindDecorator.name, () => { it('should decorate getScriptKind', () => { expect( - getScriptKindDecorator(createInfo, typescript, logger, config) + getScriptKindDecorator(createInfo, typescript, logger, config, fao) .getScriptKind, ).toBeInstanceOf(Function) }) @@ -54,6 +61,7 @@ describe(getScriptKindDecorator.name, () => { typescript, logger, config, + fao, ) expect(decorated.getScriptKind?.('foo')).toBe(typescript.ScriptKind.Unknown) }) @@ -64,6 +72,7 @@ describe(getScriptKindDecorator.name, () => { typescript, logger, config, + fao, ) expect(decorated.getScriptKind?.('foo.sol')).toBe(typescript.ScriptKind.TS) expect(decorated.getScriptKind?.('./foo.sol')).toBe( @@ -77,6 +86,7 @@ describe(getScriptKindDecorator.name, () => { typescript, logger, config, + fao, ) const expected = typescript.ScriptKind.JS createInfo.languageServiceHost.getScriptKind.mockReturnValue(expected) diff --git a/ts-plugin/src/decorators/getScriptSnapshot.spec.ts b/ts-plugin/src/decorators/getScriptSnapshot.spec.ts index 2c988db205..2a9d69fc80 100644 --- a/ts-plugin/src/decorators/getScriptSnapshot.spec.ts +++ b/ts-plugin/src/decorators/getScriptSnapshot.spec.ts @@ -1,6 +1,9 @@ import { getScriptSnapshotDecorator } from '.' import { Logger } from '../factories' +import { FileAccessObject } from '@evmts/bundler' import { EvmtsConfig, defaultConfig, defineConfig } from '@evmts/config' +import { existsSync, readFileSync } from 'fs' +import { readFile } from 'fs/promises' import path from 'path' import typescript from 'typescript/lib/tsserverlibrary' import { Mock, beforeEach, describe, expect, it, vi } from 'vitest' @@ -17,6 +20,12 @@ const mockConfig: EvmtsConfig = { } const config = defineConfig(() => mockConfig).configFn('.') +const fao: FileAccessObject = { + readFile, + readFileSync, + existsSync, +} + describe(getScriptSnapshotDecorator.name, () => { let logger: Logger let languageServiceHost: { @@ -51,6 +60,7 @@ describe(getScriptSnapshotDecorator.name, () => { typescript, logger, config, + fao, ) const fileName = 'foo.ts' const result = decorator.getScriptSnapshot(fileName) @@ -64,6 +74,7 @@ describe(getScriptSnapshotDecorator.name, () => { typescript, logger, config, + fao, ) const fileName = path.join(__dirname, '../test/fixtures/HelloWorld3.sol') decorator.getScriptSnapshot(fileName) @@ -77,6 +88,7 @@ describe(getScriptSnapshotDecorator.name, () => { typescript, logger, config, + fao, ) const fileName = path.join(__dirname, '../test/fixtures/HelloWorld.sol') decorator.getScriptSnapshot(fileName) @@ -90,6 +102,7 @@ describe(getScriptSnapshotDecorator.name, () => { typescript, logger, config, + fao, ) const fileName = path.join(__dirname, '../test/fixtures/HelloWorld2.sol') const result = decorator.getScriptSnapshot(fileName) @@ -117,6 +130,7 @@ describe(getScriptSnapshotDecorator.name, () => { typescript, logger, config, + fao, ) const fileName = path.join(__dirname, '../test/fixtures/BadCompile.sol') const result = decorator.getScriptSnapshot(fileName) diff --git a/ts-plugin/src/decorators/getScriptSnapshot.ts b/ts-plugin/src/decorators/getScriptSnapshot.ts index e27f6651b3..a8f92a4637 100644 --- a/ts-plugin/src/decorators/getScriptSnapshot.ts +++ b/ts-plugin/src/decorators/getScriptSnapshot.ts @@ -10,7 +10,7 @@ import { existsSync } from 'fs' * TODO replace with modules for code reuse */ export const getScriptSnapshotDecorator = createHostDecorator( - ({ languageServiceHost }, ts, logger, config) => { + ({ languageServiceHost }, ts, logger, config, fao) => { return { getScriptSnapshot: (filePath) => { if ( @@ -22,7 +22,7 @@ export const getScriptSnapshotDecorator = createHostDecorator( return languageServiceHost.getScriptSnapshot(filePath) } try { - const plugin = bundler(config, logger as any) + const plugin = bundler(config, logger as any, fao) const snapshot = plugin.resolveDtsSync(filePath, process.cwd(), false) return ts.ScriptSnapshot.fromString(snapshot.code) } catch (e) { diff --git a/ts-plugin/src/decorators/resolveModuleNameLiterals.spec.ts b/ts-plugin/src/decorators/resolveModuleNameLiterals.spec.ts index 04fe445b1c..6893a0f78f 100644 --- a/ts-plugin/src/decorators/resolveModuleNameLiterals.spec.ts +++ b/ts-plugin/src/decorators/resolveModuleNameLiterals.spec.ts @@ -1,5 +1,6 @@ import { resolveModuleNameLiteralsDecorator } from '.' import { solidityModuleResolver } from '../utils' +import { FileAccessObject } from '@evmts/bundler' import { EvmtsConfig, defaultConfig, defineConfig } from '@evmts/config' import typescript from 'typescript/lib/tsserverlibrary' import { MockedFunction, describe, expect, it, vi } from 'vitest' @@ -14,6 +15,12 @@ const mockConfig: EvmtsConfig = { } const config = defineConfig(() => mockConfig).configFn('.') +const fao: FileAccessObject = { + existsSync: vi.fn(), + readFileSync: vi.fn(), + readFile: vi.fn(), +} + const mockSolidityModuleResolver = solidityModuleResolver as MockedFunction< typeof solidityModuleResolver > @@ -51,6 +58,7 @@ describe(resolveModuleNameLiteralsDecorator.name, () => { typescript, logger, config, + fao, ) expect(host).toMatchInlineSnapshot(` @@ -101,6 +109,7 @@ describe(resolveModuleNameLiteralsDecorator.name, () => { typescript, logger, config, + fao, ) const moduleNames = [{ text: 'moduleName' }] @@ -153,6 +162,7 @@ describe(resolveModuleNameLiteralsDecorator.name, () => { typescript, logger, config, + fao, ) const moduleNames = [{ text: 'moduleName' }] diff --git a/ts-plugin/src/factories/decorator.spec.ts b/ts-plugin/src/factories/decorator.spec.ts index 9a5b89ab68..a7873177af 100644 --- a/ts-plugin/src/factories/decorator.spec.ts +++ b/ts-plugin/src/factories/decorator.spec.ts @@ -4,6 +4,7 @@ import { createHostDecorator, decorateHost, } from '.' +import { FileAccessObject } from '@evmts/bundler' import { EvmtsConfig, defaultConfig, defineConfig } from '@evmts/config' import typescript from 'typescript/lib/tsserverlibrary' import { describe, expect, it, vi } from 'vitest' @@ -22,6 +23,12 @@ const mockConfig: EvmtsConfig = { const config = defineConfig(() => mockConfig).configFn('.') +const fao: FileAccessObject = { + existsSync: vi.fn(), + readFile: vi.fn(), + readFileSync: vi.fn(), +} + const createProxy = (instance: T, proxy: Partial): T => { return new Proxy(instance, { get(target, key) { @@ -57,7 +64,7 @@ describe(createHostDecorator.name, () => { warn: vi.fn(), } as any - const host = decorator(createInfo, typescript, logger, config) + const host = decorator(createInfo, typescript, logger, config, fao) expect(host.getScriptKind?.('foo.json')).toBe(typescript.ScriptKind.JSON) expect(host.getScriptKind?.('foo.ts')).toBe(typescript.ScriptKind.TS) @@ -98,6 +105,7 @@ describe(decorateHost.name, () => { typescript, logger, config, + fao, ) expect((decoratedHost as TestAny).isHost).toBe(true) @@ -120,6 +128,7 @@ describe(decorateHost.name, () => { typescript, logger, config, + fao, ) expect(decoratedHost).toBe(host) }) @@ -144,6 +153,7 @@ describe(decorateHost.name, () => { typescript, logger, config, + fao, ) // Check that the non-languageServiceHost property 'isCreateInfo' has been preserved diff --git a/ts-plugin/src/factories/decorator.ts b/ts-plugin/src/factories/decorator.ts index 0358dbaa3c..db9083e398 100644 --- a/ts-plugin/src/factories/decorator.ts +++ b/ts-plugin/src/factories/decorator.ts @@ -1,4 +1,5 @@ import type { Logger } from './logger' +import { FileAccessObject } from '@evmts/bundler' import { ResolvedConfig } from '@evmts/config' import type typescript from 'typescript/lib/tsserverlibrary' @@ -12,6 +13,7 @@ export type HostDecorator = ( ts: typeof typescript, logger: Logger, config: ResolvedConfig, + fao: FileAccessObject, ) => typescript.LanguageServiceHost /** @@ -32,6 +34,7 @@ export type PartialHostDecorator = ( ts: typeof typescript, logger: Logger, config: ResolvedConfig, + fao: FileAccessObject, ) => Partial /** diff --git a/ts-plugin/src/factories/fileAccessObject.spec.ts b/ts-plugin/src/factories/fileAccessObject.spec.ts new file mode 100644 index 0000000000..fd132a67aa --- /dev/null +++ b/ts-plugin/src/factories/fileAccessObject.spec.ts @@ -0,0 +1,58 @@ +import { createFileAccessObject } from './fileAccessObject' +import { LanguageServiceHost } from 'typescript' +import { describe, expect, it, vi } from 'vitest' + +// Mock the LanguageServiceHost +const mockLsHost = (fileContent: string | null): LanguageServiceHost => + ({ + readFile: vi.fn().mockImplementation((fileName, encoding) => fileContent), + fileExists: vi.fn().mockImplementation((fileName) => fileContent !== null), + }) as any + +describe('createFileAccessObject', () => { + it('should read file asynchronously', async () => { + const fileContent = 'file content here' + const lsHost = mockLsHost(fileContent) + const fileAccessObject = createFileAccessObject(lsHost) + + const result = await fileAccessObject.readFile('test.ts', 'utf8') + expect(result).toBe(fileContent) + }) + + it('should throw error when unable to read file asynchronously', async () => { + const lsHost = mockLsHost(null) // Simulate no file content + const fileAccessObject = createFileAccessObject(lsHost) + + expect( + fileAccessObject.readFile('test.ts', 'utf8'), + ).rejects.toThrowErrorMatchingInlineSnapshot( + '"@evmts/ts-plugin: unable to read file test.ts"', + ) + }) + + it('should check if file exists synchronously', () => { + const lsHost = mockLsHost('file content') // Simulate file exists + const fileAccessObject = createFileAccessObject(lsHost) + + const result = fileAccessObject.existsSync('test.ts') + expect(result).toBe(true) + }) + + it('should read file synchronously', () => { + const fileContent = 'file content here' + const lsHost = mockLsHost(fileContent) + const fileAccessObject = createFileAccessObject(lsHost) + + const result = fileAccessObject.readFileSync('test.ts', 'utf8') + expect(result).toBe(fileContent) + }) + + it('should throw error when unable to read file synchronously', () => { + const lsHost = mockLsHost(null) // Simulate no file content + const fileAccessObject = createFileAccessObject(lsHost) + + expect(() => { + fileAccessObject.readFileSync('test.ts', 'utf8') + }).toThrow('@evmts/ts-plugin: unable to read file test.ts') + }) +}) diff --git a/ts-plugin/src/factories/fileAccessObject.ts b/ts-plugin/src/factories/fileAccessObject.ts new file mode 100644 index 0000000000..55a69ad15f --- /dev/null +++ b/ts-plugin/src/factories/fileAccessObject.ts @@ -0,0 +1,24 @@ +import { FileAccessObject } from '@evmts/bundler' +import typescript from 'typescript/lib/tsserverlibrary' + +export const createFileAccessObject = ( + lsHost: typescript.LanguageServiceHost, +): FileAccessObject => { + return { + readFile: async (fileName, encoding) => { + const file = lsHost.readFile(fileName, encoding) + if (!file) { + throw new Error(`@evmts/ts-plugin: unable to read file ${fileName}`) + } + return file + }, + existsSync: (fileName) => lsHost.fileExists(fileName), + readFileSync: (fileName, encoding) => { + const file = lsHost.readFile(fileName, encoding) + if (!file) { + throw new Error(`@evmts/ts-plugin: unable to read file ${fileName}`) + } + return file + }, + } +} diff --git a/ts-plugin/src/tsPlugin.ts b/ts-plugin/src/tsPlugin.ts index a7b65a2469..4709516ecb 100644 --- a/ts-plugin/src/tsPlugin.ts +++ b/ts-plugin/src/tsPlugin.ts @@ -5,6 +5,7 @@ import { } from './decorators' import { getDefinitionServiceDecorator } from './decorators/getDefinitionAtPosition' import { createLogger, decorateHost } from './factories' +import { createFileAccessObject } from './factories/fileAccessObject' import { isSolidity } from './utils' import { loadConfig } from '@evmts/config' import typescript from 'typescript/lib/tsserverlibrary' @@ -26,17 +27,19 @@ export const tsPlugin: typescript.server.PluginModuleFactory = (modules) => { createInfo.project.getCurrentDirectory(), logger, ) + const fao = createFileAccessObject(createInfo.languageServiceHost) const service = getDefinitionServiceDecorator( modules.typescript.createLanguageService( decorateHost( getScriptKindDecorator, resolveModuleNameLiteralsDecorator, getScriptSnapshotDecorator, - )(createInfo, modules.typescript, logger, config), + )(createInfo, modules.typescript, logger, config, fao), ), config, logger, modules.typescript, + fao, ) return service diff --git a/ts-plugin/vitest.config.ts b/ts-plugin/vitest.config.ts index d5ab7394b5..50c7cbf228 100644 --- a/ts-plugin/vitest.config.ts +++ b/ts-plugin/vitest.config.ts @@ -6,10 +6,10 @@ export default defineConfig({ include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], coverage: { reporter: ['text', 'json-summary', 'json'], - lines: 94.24, - branches: 86.66, + lines: 94.52, + branches: 87.38, functions: 100, - statements: 94.24, + statements: 94.52, thresholdAutoUpdate: true, }, },