Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"scripts": {
"prepare": "yarn build",
"build": "rm -rf dist && tsc",
"build": "rm -rf dist && tsc && cp src/lib-runner-mapping.yaml dist/lib-runner-mapping.yaml",
"start": "yarn build && node ./bin/run.js",
"test": "yarn build && ts-mocha ./tests --recursive --extension .spec.ts --exit --timeout 5000",
"lint": "eslint ."
Expand All @@ -28,6 +28,7 @@
"form-data": "^4.0.1",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"semver": "^7.6.3",
"simple-git": "^3.30.0",
"zod": "^3.24.1"
},
Expand All @@ -40,6 +41,7 @@
"@types/lodash": "^4.17.15",
"@types/mocha": "^10.0.1",
"@types/node": "^22.10.5",
"@types/semver": "^7.5.8",
"axios-mock-adapter": "^2.1.0",
"chai": "^4.3.7",
"eslint-config-mimic": "^0.0.3",
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/lib-runner-mapping.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Mapping between @mimicprotocol/lib-ts versions and runner versions
# Only add new entries when there are breaking changes between lib-ts and runner
#
# Format:
# - libVersionRange: semver range (e.g., ">=0.0.1-rc.1 <0.1.0")
# runnerVersion: semver version (e.g., "1.0.0")
#
# Example with multiple entries:
# - libVersionRange: ">=0.0.1-rc.1 <0.1.0"
# runnerVersion: "1.0.0"
# - libVersionRange: ">=0.1.0"
# runnerVersion: "2.0.0"

- libVersionRange: ">=0.0.1-rc.1"
runnerVersion: "1.0.0"
51 changes: 42 additions & 9 deletions packages/cli/src/lib/ManifestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { Command } from '@oclif/core'
import * as fs from 'fs'
import { load } from 'js-yaml'
import * as path from 'path'
import * as semver from 'semver'
import { ZodError } from 'zod'

import { DuplicateEntryError, EmptyManifestError, MoreThanOneEntryError } from '../errors'
import { Manifest } from '../types'
import { ManifestValidator } from '../validators'
import { LibRunnerMappingValidator, ManifestValidator } from '../validators'

export default {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -17,7 +18,7 @@ export default {
...manifest,
inputs: mergeIfUnique(manifest.inputs),
abis: mergeIfUnique(manifest.abis),
metadata: { libVersion: getLibVersion() },
metadata: { runnerVersion: getRunnerVersion(getLibVersion()) },
}
return ManifestValidator.parse(mergedManifest)
},
Expand Down Expand Up @@ -80,17 +81,49 @@ function handleValidationError(command: Command, err: unknown): never {
command.error(message, { code, suggestions })
}

function findFileInNodeModules(relativePath: string): string {
let currentDir = process.cwd()

while (currentDir !== path.dirname(currentDir)) {
const filePath = path.join(currentDir, 'node_modules', ...relativePath.split('/'))
if (fs.existsSync(filePath)) return filePath
currentDir = path.dirname(currentDir)
}

throw new Error(`Could not find ${relativePath} in node_modules`)
}

function getLibVersion(): string {
try {
let currentDir = process.cwd()
while (currentDir !== path.dirname(currentDir)) {
const libPackagePath = path.join(currentDir, 'node_modules', '@mimicprotocol', 'lib-ts', 'package.json')
if (fs.existsSync(libPackagePath)) return JSON.parse(fs.readFileSync(libPackagePath, 'utf-8')).version
currentDir = path.dirname(currentDir)
const libPackagePath = findFileInNodeModules('@mimicprotocol/lib-ts/package.json')
const packageContent = fs.readFileSync(libPackagePath, 'utf-8')
return JSON.parse(packageContent).version
} catch (error) {
if (error instanceof Error && error.message.includes('Could not find')) {
throw new Error('Could not find @mimicprotocol/lib-ts package')
}
throw new Error(`Failed to read @mimicprotocol/lib-ts version: ${error}`)
}
}

export function getRunnerVersion(libVersion: string, mappingPath?: string): string {
try {
const resolvedMappingPath = mappingPath || findFileInNodeModules('@mimicprotocol/cli/dist/lib-runner-mapping.yaml')

const mappingContent = fs.readFileSync(resolvedMappingPath, 'utf-8')
const mapping = LibRunnerMappingValidator.parse(load(mappingContent))

for (const entry of mapping) {
if (semver.satisfies(libVersion, entry.libVersionRange)) return entry.runnerVersion
}

throw new Error('Could not find @mimicprotocol/lib-ts package')
throw new Error(`No runner version mapping found for lib-ts version ${libVersion}`)
} catch (error) {
throw new Error(`Failed to read @mimicprotocol/lib-ts version: ${error}`)
if (error instanceof ZodError) {
throw new Error(
`Failed to read lib-runner-mapping.yaml from @mimicprotocol/cli: Invalid format - ${error.message}`
)
}
throw error
}
}
3 changes: 2 additions & 1 deletion packages/cli/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { z } from 'zod'

import { ManifestValidator } from './validators'
import { LibRunnerMappingValidator, ManifestValidator } from './validators'

export type Manifest = z.infer<typeof ManifestValidator>
export type ManifestInputs = z.infer<typeof ManifestValidator.shape.inputs>
export type LibRunnerMapping = z.infer<typeof LibRunnerMappingValidator>

export type AbiParameter = {
name?: string
Expand Down
12 changes: 10 additions & 2 deletions packages/cli/src/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,27 @@ export const SEM_VER_REGEX =

const String = z.string().min(1)

const SemVer = String.regex(SEM_VER_REGEX, 'Must be a valid semver')
const SolidityType = String.regex(SOLIDITY_TYPE_REGEX, 'Must be a valid solidity type')
const TokenType = String.regex(TOKEN_TYPE_REGEX, 'Must be a valid token type')
const InputType = z.union([SolidityType, TokenType])

const InputValue = z.union([InputType, z.object({ type: InputType, description: String.optional() })])

export const ManifestValidator = z.object({
version: String.regex(SEM_VER_REGEX, 'Must be a valid semver'),
version: SemVer,
name: String,
description: String,
inputs: z.record(String, InputValue),
abis: z.record(String, String),
metadata: z.object({
libVersion: String.regex(SEM_VER_REGEX, 'Must be a valid semver'),
runnerVersion: SemVer,
}),
})

export const LibRunnerMappingValidator = z.array(
z.object({
libVersionRange: String,
runnerVersion: SemVer,
})
)
114 changes: 103 additions & 11 deletions packages/cli/tests/ManifestHandler.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from 'chai'
import * as path from 'path'

import ManifestHandler from '../src/lib/ManifestHandler'
import ManifestHandler, { getRunnerVersion } from '../src/lib/ManifestHandler'
import { SEM_VER_REGEX } from '../src/validators'

import invalidSemVers from './fixtures/sem-vers/invalid-sem-vers.json'
Expand Down Expand Up @@ -71,22 +72,22 @@ describe('ManifestHandler', () => {
})
})

context('when dealing with lib version', () => {
context('when the lib version is not present', () => {
it('adds the lib version to the manifest', () => {
context('when dealing with runner version', () => {
context('when the runner version is not present', () => {
it('adds the runner version to the manifest', () => {
const parsedManifest = ManifestHandler.validate(manifest)

expect(parsedManifest.metadata.libVersion).to.match(SEM_VER_REGEX)
expect(parsedManifest.metadata.runnerVersion).to.match(SEM_VER_REGEX)
})
})

context('when the lib version is present', () => {
it('overrides the lib version', () => {
const libVersion = '999.9.9'
const manifestWithLibVersion = { ...manifest, metadata: { libVersion } }
const parsedManifest = ManifestHandler.validate(manifestWithLibVersion)
context('when the runner version is present', () => {
it('overrides the runner version', () => {
const runnerVersion = '999.9.9'
const manifestWithRunnerVersion = { ...manifest, metadata: { runnerVersion } }
const parsedManifest = ManifestHandler.validate(manifestWithRunnerVersion)

expect(parsedManifest.metadata.libVersion).to.not.equal(libVersion)
expect(parsedManifest.metadata.runnerVersion).to.not.equal(runnerVersion)
})
})
})
Expand Down Expand Up @@ -151,4 +152,95 @@ describe('ManifestHandler', () => {
})
})
})

describe('getRunnerVersion', () => {
const singleRangePath = path.join(__dirname, 'fixtures', 'lib-runner-mappings', 'single-range.yaml')
const multipleRangesPath = path.join(__dirname, 'fixtures', 'lib-runner-mappings', 'multiple-ranges.yaml')
const invalidMappingPath = path.join(__dirname, 'fixtures', 'lib-runner-mappings', 'invalid-mapping.yaml')

context('when using a single range mapping', () => {
context('when version satisfies the range', () => {
context('when version is at exact boundary', () => {
it('returns the corresponding runner version', () => {
const result = getRunnerVersion('0.0.1-rc.1', singleRangePath)

expect(result).to.equal('1.0.0')
})
})

context('when version is above the boundary', () => {
it('returns the corresponding runner version', () => {
const versions = ['0.0.1-rc.5', '0.1.0', '1.0.0', '2.0.0']

for (const version of versions) {
const result = getRunnerVersion(version, singleRangePath)
expect(result).to.equal('1.0.0')
}
})
})
})

context('when version does not satisfy the range', () => {
context('when version is below the boundary', () => {
it('throws an error', () => {
const versions = ['0.0.0', '0.0.1-alpha.1', '0.0.1-beta.1', '0.0.1-rc.0']

for (const version of versions) {
expect(() => getRunnerVersion(version, singleRangePath)).to.throw(
`No runner version mapping found for lib-ts version ${version}`
)
}
})
})
})
})

context('when using multiple range mappings', () => {
context('when version satisfies multiple ranges', () => {
it('returns the runner version from the first matching range', () => {
const result = getRunnerVersion('0.0.5', multipleRangesPath)

expect(result).to.equal('1.0.0')
})
})

context('when version satisfies only one range', () => {
it('returns the corresponding runner version', () => {
const testCases = [
{ version: '0.0.1-rc.1', expected: '1.0.0' },
{ version: '0.0.9', expected: '1.0.0' },
{ version: '0.1.0', expected: '2.0.0' },
{ version: '0.5.0', expected: '2.0.0' },
{ version: '1.0.0', expected: '3.0.0' },
{ version: '2.0.0', expected: '3.0.0' },
]

for (const { version, expected } of testCases) {
const result = getRunnerVersion(version, multipleRangesPath)
expect(result).to.equal(expected)
}
})
})

context('when version does not satisfy any range', () => {
it('throws an error', () => {
const versions = ['0.0.0', '0.0.1-alpha.1', '0.0.1-beta.1']

for (const version of versions) {
expect(() => getRunnerVersion(version, multipleRangesPath)).to.throw(
`No runner version mapping found for lib-ts version ${version}`
)
}
})
})
})

context('when mapping file is invalid', () => {
it('throws an error', () => {
expect(() => getRunnerVersion('0.0.1-rc.1', invalidMappingPath)).to.throw(
'Failed to read lib-runner-mapping.yaml'
)
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Invalid mapping fixture - missing required fields
- libVersionRange: ">=0.0.1-rc.1"
# runnerVersion is missing
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Multiple range mappings fixture for testing priority/order
- libVersionRange: ">=1.0.0"
runnerVersion: "3.0.0"
- libVersionRange: ">=0.1.0 <1.0.0"
runnerVersion: "2.0.0"
- libVersionRange: ">=0.0.1-rc.1 <0.1.0"
runnerVersion: "1.0.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Single range mapping fixture (simulates current state)
- libVersionRange: ">=0.0.1-rc.1"
runnerVersion: "1.0.0"
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,11 @@
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.0.tgz#64c441bdae033b378b6eef7d0c3d77c329b9378e"
integrity sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==

"@types/semver@^7.5.8":
version "7.7.1"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.1.tgz#3ce3af1a5524ef327d2da9e4fd8b6d95c8d70528"
integrity sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==

"@types/send@*":
version "0.17.5"
resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74"
Expand Down