Skip to content

Commit

Permalink
✨ feat(ts-plugin): Enable in memory EVMts typeserver (#466)
Browse files Browse the repository at this point in the history
## Description

- Update @evmts/bundler to take a file-access-object as a param instead
of using `fs`
- Update @evmts/bun-plugin to pass in a file-access-object using native
bun methods for peformance
- Update @evmts/ts-plugin to pass in Ts server files as the
file-access-object

Bun is faster from using native bun files instead of normal files. In
future we should make the async methods truly async for even better
peformance.

Ts-plugin will now work without clicking save since files are accessed
through ts-server instead of file system

## Testing

Existing unit tests and more testing for newly implemented features

## Additional Information

- [ ] I read the [contributing docs](../docs/contributing.md) (if this
is your first contribution)

Your ENS/address:

Co-authored-by: Will Cory <[email protected]>
  • Loading branch information
roninjin10 and Will Cory authored Sep 10, 2023
1 parent ce27a61 commit 1c4cbd2
Show file tree
Hide file tree
Showing 35 changed files with 499 additions and 68 deletions.
15 changes: 15 additions & 0 deletions .changeset/kind-eggs-mate.md
Original file line number Diff line number Diff line change
@@ -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

10 changes: 10 additions & 0 deletions .changeset/lucky-jobs-yell.md
Original file line number Diff line number Diff line change
@@ -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`

6 changes: 6 additions & 0 deletions .changeset/moody-pets-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@evmts/bun-plugin": patch
---

Updated Bun to use native Bun.file api which is more peformant than using `fs`

1 change: 1 addition & 0 deletions bundlers/bun/src/bunFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { file } from 'bun'
98 changes: 98 additions & 0 deletions bundlers/bun/src/bunFileAccessObject.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
18 changes: 18 additions & 0 deletions bundlers/bun/src/bunFileAccessObject.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>
} = {
existsSync,
exists: (filePath: string) => {
const bunFile = file(filePath)
return bunFile.exists()
},
readFile: (filePath: string) => {
const bunFile = file(filePath)
return bunFile.text()
},
readFileSync,
}
13 changes: 12 additions & 1 deletion bundlers/bun/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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 () => ({
Expand All @@ -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
Expand All @@ -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({
Expand Down
12 changes: 7 additions & 5 deletions bundlers/bun/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
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 = {}

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) {
Expand Down Expand Up @@ -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],
}
}
Expand Down
1 change: 1 addition & 0 deletions bundlers/bun/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export default defineConfig({
splitting: false,
sourcemap: true,
clean: true,
bundle: false,
})
10 changes: 8 additions & 2 deletions bundlers/bundler/src/bundler.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down
74 changes: 65 additions & 9 deletions bundlers/bundler/src/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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'`
Expand All @@ -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 }
},
Expand Down
Loading

1 comment on commit 1c4cbd2

@vercel
Copy link

@vercel vercel bot commented on 1c4cbd2 Sep 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

evmts-docs – ./

evmts-docs-evmts.vercel.app
evmts-docs-git-main-evmts.vercel.app
evmts.dev

Please sign in to comment.