Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
b79f0fa
testing
Oct 7, 2025
6767b3c
import guessing
Oct 7, 2025
4c54cb2
rewriting
Oct 7, 2025
140e63c
Delete libs/remix-solidity/IMPORT_CALLBACK_EXAMPLE.md
bunsenstraat Oct 7, 2025
dd39c0b
Delete RESOLVER_TARGET_CHANGES.md
bunsenstraat Oct 7, 2025
9477b5d
Delete libs/remix-solidity/QUICK_START_TARGET.md
bunsenstraat Oct 7, 2025
8923168
fix test
Oct 7, 2025
ec0c428
init
Oct 7, 2025
d4fb244
fixed
Oct 7, 2025
87a5280
Merge branch 'master' into resolver2
bunsenstraat Oct 7, 2025
6da609d
bugfixes
Oct 7, 2025
7fa55bc
Merge branch 'resolver2' of https://github.com/remix-project-org/remi…
Oct 7, 2025
737489f
master index
Oct 7, 2025
7b28515
go to definition
Oct 7, 2025
7c4693e
peer deps
Oct 8, 2025
0a46495
version warnings
Oct 8, 2025
75e0146
clean up import manager
Oct 8, 2025
6d0bc66
message
Oct 8, 2025
5fc9d05
fix warnings
Oct 8, 2025
9e8d10f
types
Oct 8, 2025
8846835
move resolutionIndex
Oct 8, 2025
fa3b062
refactor
Oct 8, 2025
3416297
cleanup
Oct 8, 2025
7fbfd0f
warnings update
Oct 8, 2025
8bea464
refactor
Oct 8, 2025
4ef2dc6
rm unneeded param
Oct 8, 2025
06be177
rm param
Oct 8, 2025
77656a5
feat(import-resolver): fix lock file parsing and workspace deps - yar…
Oct 8, 2025
137d1d9
test(e2e): add import resolver E2E tests - groups 1-5 all working
Oct 8, 2025
6ecea1f
fix(import-resolver): reload lock files on each resolution
Oct 8, 2025
a534479
test(e2e): add lock file change detection tests
Oct 8, 2025
a905de1
fix(e2e): use openFile + setEditorValue to modify lock files
Oct 8, 2025
f32a641
fix tests
Oct 8, 2025
05e6145
tests pass
Oct 8, 2025
62310b3
Delete apps/remix-ide-e2e/IMPORT_RESOLVER_TESTS.md
bunsenstraat Oct 8, 2025
ae8e967
Delete apps/remix-ide-e2e/TEST_RESULTS_SUMMARY.md
bunsenstraat Oct 8, 2025
8244b1c
Delete apps/remix-ide-e2e/src/tests/importRewrite.test.ts
bunsenstraat Oct 8, 2025
b8c3a42
Delete package-lock.json
bunsenstraat Oct 8, 2025
9e9d111
disable main test
Oct 8, 2025
b0c3016
fix test
Oct 8, 2025
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
380 changes: 380 additions & 0 deletions apps/remix-ide-e2e/src/tests/importResolver.test.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'
import { CompilerAbstract } from '@remix-project/remix-solidity'
import { Compiler } from '@remix-project/remix-solidity'
import { Compiler, ImportResolver } from '@remix-project/remix-solidity'

import { CompilationResult, CompilationSource } from '@remix-project/remix-solidity'
import { CodeParser } from "../code-parser";
Expand Down Expand Up @@ -119,7 +119,13 @@ export default class CodeParserCompiler {
this.plugin.emit('astFinished')
}

this.compiler = new Compiler((url, cb) => this.plugin.call('contentImport', 'resolveAndSave', url, undefined).then((result) => cb(null, result)).catch((error) => cb(error.message)))
this.compiler = new Compiler(
(url, cb) => { return this.plugin.call('contentImport', 'resolveAndSave', url).then((result) => cb(null, result)).catch((error: Error) => cb(error.message)) },
(target) => {
// Factory function: creates a new ImportResolver for each compilation
return new ImportResolver(this.plugin, target)
}
)
this.compiler.event.register('compilationFinished', this.onAstFinished)
}

Expand Down
3 changes: 2 additions & 1 deletion apps/remix-ide/src/app/tabs/compile-tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export default class CompileTab extends CompilerApiMixin(ViewPlugin) { // implem
this.fileManager = fileManager
this.config = config
this.queryParams = new QueryParams()
this.compileTabLogic = new CompileTabLogic(this, this.contentImport)
// Pass 'this' as the plugin reference so CompileTabLogic can access contentImport via this.call()
this.compileTabLogic = new CompileTabLogic(this)
this.compiler = this.compileTabLogic.compiler
this.compileTabLogic.init()
this.initCompilerApi()
Expand Down
2 changes: 1 addition & 1 deletion apps/solidity-compiler/src/app/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class CompilerClientApi extends CompilerApiMixin(PluginClient) implements
constructor () {
super()
createClient(this as any)
this.compileTabLogic = new CompileTabLogic(this, this.contentImport)
this.compileTabLogic = new CompileTabLogic(this)
this.compiler = this.compileTabLogic.compiler
this.compileTabLogic.init()
this.initCompilerApi()
Expand Down
90 changes: 80 additions & 10 deletions libs/remix-core-plugin/src/lib/compiler-content-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const profile = {
name: 'contentImport',
displayName: 'content import',
version: '0.0.1',
methods: ['resolve', 'resolveAndSave', 'isExternalUrl', 'resolveGithubFolder']
methods: ['resolve', 'resolveAndSave', 'isExternalUrl', 'resolveGithubFolder', 'resolveImportFromIndex']
}

export type ResolvedImport = {
Expand All @@ -17,6 +17,7 @@ export type ResolvedImport = {

export class CompilerImports extends Plugin {
urlResolver: any

constructor () {
super(profile)
this.urlResolver = new RemixURLResolver(async () => {
Expand Down Expand Up @@ -49,7 +50,9 @@ export class CompilerImports extends Plugin {

onActivation(): void {
const packageFiles = ['package.json', 'package-lock.json', 'yarn.lock']
this.on('filePanel', 'setWorkspace', () => this.urlResolver.clearCache())
this.on('filePanel', 'setWorkspace', () => {
this.urlResolver.clearCache()
})
this.on('fileManager', 'fileRemoved', (file: string) => {
if (packageFiles.includes(file)) {
this.urlResolver.clearCache()
Expand All @@ -62,6 +65,53 @@ export class CompilerImports extends Plugin {
})
}

/**
* Resolve an import path using the persistent resolution index
* This is used by the editor for "Go to Definition" navigation
*/
async resolveImportFromIndex(sourceFile: string, importPath: string): Promise<string | null> {
const indexPath = '.deps/npm/.resolution-index.json'

try {
// Just read the file directly!
const exists = await this.call('fileManager', 'exists', indexPath)
if (!exists) {
console.log('[CompilerImports] ℹ️ No resolution index file found')
return null
}

const content = await this.call('fileManager', 'readFile', indexPath)
const index = JSON.parse(content)

console.log('[CompilerImports] 🔍 Looking up:', { sourceFile, importPath })
console.log('[CompilerImports] 📊 Index has', Object.keys(index).length, 'source files')

// First try: lookup using the current file (works if currentFile is a base file)
if (index[sourceFile] && index[sourceFile][importPath]) {
const resolved = index[sourceFile][importPath]
console.log('[CompilerImports] ✅ Direct lookup result:', resolved)
return resolved
}

// Second try: search across ALL base files (works if currentFile is a library file)
console.log('[CompilerImports] 🔍 Trying lookupAny across all source files...')
for (const file in index) {
if (index[file][importPath]) {
const resolved = index[file][importPath]
console.log('[CompilerImports] ✅ Found in', file, ':', resolved)
return resolved
}
}

console.log('[CompilerImports] ℹ️ Import not found in index')
return null

} catch (err) {
console.log('[CompilerImports] ⚠️ Failed to read resolution index:', err)
return null
}
}

async setToken () {
try {
const protocol = typeof window !== 'undefined' && window.location.protocol
Expand All @@ -84,11 +134,11 @@ export class CompilerImports extends Plugin {
}

/**
* resolve the content of @arg url. This only resolves external URLs.
*
* @param {String} url - external URL of the content. can be basically anything like raw HTTP, ipfs URL, github address etc...
* @returns {Promise} - { content, cleanUrl, type, url }
*/
* resolve the content of @arg url. This only resolves external URLs.
*
* @param {String} url - external URL of the content. can be basically anything like raw HTTP, ipfs URL, github address etc...
* @returns {Promise} - { content, cleanUrl, type, url }
*/
resolve (url) {
return new Promise((resolve, reject) => {
this.import(url, null, (error, content, cleanUrl, type, url) => {
Expand All @@ -108,8 +158,6 @@ export class CompilerImports extends Plugin {
if (!loadingCb) loadingCb = () => {}
if (!cb) cb = () => {}

const self = this

let resolved
try {
await this.setToken()
Expand Down Expand Up @@ -140,6 +188,27 @@ export class CompilerImports extends Plugin {
})
}

/**
* import the content of @arg url. /**
* resolve the content of @arg url. This only resolves external URLs.
return new Promise((resolve, reject) => {
this.import(url,
// TODO: handle this event
(loadingMsg) => { this.emit('message', loadingMsg) },
async (error, content, cleanUrl, type, url) => {
if (error) return reject(error)
try {
const provider = await this.call('fileManager', 'getProviderOf', null)
const path = targetPath || type + '/' + cleanUrl
if (provider) await provider.addExternal('.deps/' + path, content, url)
} catch (err) {
console.error(err)
}
resolve(content)
}, null)
})
}

/**
* import the content of @arg url.
* first look in the browser localstorage (browser explorer) or localhost explorer. if the url start with `browser/*` or `localhost/*`
Expand All @@ -148,9 +217,10 @@ export class CompilerImports extends Plugin {
*
* @param {String} url - URL of the content. can be basically anything like file located in the browser explorer, in the localhost explorer, raw HTTP, github address etc...
* @param {String} targetPath - (optional) internal path where the content should be saved to
* @param {Boolean} skipMappings - (optional) unused parameter, kept for backward compatibility
* @returns {Promise} - string content
*/
async resolveAndSave (url, targetPath) {
async resolveAndSave (url, targetPath, skipMappings = false) {
try {
if (targetPath && this.currentRequest) {
const canCall = await this.askUserPermission('resolveAndSave', 'This action will update the path ' + targetPath)
Expand Down
118 changes: 112 additions & 6 deletions libs/remix-solidity/src/compiler/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { update } from 'solc/abi'
import compilerInput, { compilerInputForConfigFile } from './compiler-input'
import EventManager from '../lib/eventManager'
import txHelper from './helper'
import { IImportResolver } from './import-resolver-interface'

import {
Source, SourceWithTarget, MessageFromWorker, CompilerState, CompilationResult,
Expand All @@ -20,9 +21,20 @@ export class Compiler {
state: CompilerState
handleImportCall
workerHandler: EsWebWorkerHandlerInterface
constructor(handleImportCall?: (fileurl: string, cb) => void) {
importResolverFactory: ((target: string) => IImportResolver) | null // Factory to create resolvers per compilation
currentResolver: IImportResolver | null // Current compilation's import resolver

constructor(
handleImportCall?: (fileurl: string, cb) => void,
importResolverFactory?: (target: string) => IImportResolver
) {
this.event = new EventManager()
this.handleImportCall = handleImportCall
this.importResolverFactory = importResolverFactory || null
this.currentResolver = null

console.log(`[Compiler] 🏗️ Constructor: importResolverFactory provided:`, !!importResolverFactory)

this.state = {
viaIR: false,
compileJSON: null,
Expand Down Expand Up @@ -86,11 +98,19 @@ export class Compiler {
if (timeStamp < this.state.compilationStartTime && this.state.compilerRetriggerMode == CompilerRetriggerMode.retrigger ) {
return
}
const fileCount = Object.keys(files).length
const missingCount = missingInputs?.length || 0
console.log(`[Compiler] 🔄 internalCompile called with ${fileCount} file(s), ${missingCount} missing input(s) to resolve`)

this.gatherImports(files, missingInputs, (error, input) => {
if (error) {
console.log(`[Compiler] ❌ gatherImports failed:`, error)
this.state.lastCompilationResult = null
this.event.trigger('compilationFinished', [false, { error: { formattedMessage: error, severity: 'error' } }, files, input, this.state.currentVersion])
} else if (this.state.compileJSON && input) { this.state.compileJSON(input, timeStamp) }
} else if (this.state.compileJSON && input) {
console.log(`[Compiler] ✅ All imports gathered, sending ${Object.keys(input.sources).length} file(s) to compiler`)
this.state.compileJSON(input, timeStamp)
}
})
}

Expand All @@ -101,6 +121,23 @@ export class Compiler {
*/

compile(files: Source, target: string): void {
console.log(`\n${'='.repeat(80)}`)
console.log(`[Compiler] 🚀 Starting NEW compilation for target: "${target}"`)
console.log(`[Compiler] 📁 Initial files provided: ${Object.keys(files).length}`)
console.log(`[Compiler] 🔌 importResolverFactory available:`, !!this.importResolverFactory)

// Create a fresh ImportResolver instance for this compilation
// This ensures complete isolation of import mappings per compilation
if (this.importResolverFactory) {
this.currentResolver = this.importResolverFactory(target)
console.log(`[Compiler] 🆕 Created new resolver instance for this compilation`)
} else {
this.currentResolver = null
console.log(`[Compiler] ⚠️ No resolver factory - import resolution will use legacy callback`)
}

console.log(`${'='.repeat(80)}\n`)

this.state.target = target
this.state.compilationStartTime = new Date().getTime()
this.event.trigger('compilationStarted', [])
Expand Down Expand Up @@ -173,12 +210,35 @@ export class Compiler {
if (data.errors) data.errors.forEach((err) => checkIfFatalError(err))
if (!noFatalErrors) {
// There are fatal errors, abort here
console.log(`[Compiler] ❌ Compilation failed with errors for target: "${this.state.target}"`)

// Clean up resolver on error
if (this.currentResolver) {
console.log(`[Compiler] 🧹 Compilation failed, discarding resolver`)
this.currentResolver = null
}

this.state.lastCompilationResult = null
this.event.trigger('compilationFinished', [false, data, source, input, version])
} else if (missingInputs !== undefined && missingInputs.length > 0 && source && source.sources) {
// try compiling again with the new set of inputs
console.log(`[Compiler] 🔄 Compilation round complete, but found ${missingInputs.length} missing input(s):`, missingInputs)
console.log(`[Compiler] 🔁 Re-compiling with new imports (sequential resolution will start)...`)
// Keep resolver alive for next round
this.internalCompile(source.sources, missingInputs, timeStamp)
} else {
console.log(`[Compiler] ✅ 🎉 Compilation successful for target: "${this.state.target}"`)

// Save resolution index before cleaning up resolver
if (this.currentResolver) {
console.log(`[Compiler] 💾 Saving resolution index...`)
this.currentResolver.saveResolutionsToIndex().catch(err => {
console.log(`[Compiler] ⚠️ Failed to save resolution index:`, err)
})
console.log(`[Compiler] 🧹 Compilation successful, discarding resolver`)
this.currentResolver = null
}

data = this.updateInterface(data)
if (source) {
source.target = this.state.target
Expand Down Expand Up @@ -372,24 +432,70 @@ export class Compiler {

gatherImports(files: Source, importHints?: string[], cb?: gatherImportsCallbackInterface): void {
importHints = importHints || []
const remainingCount = importHints.length

if (remainingCount > 0) {
console.log(`[Compiler] 📦 gatherImports: ${remainingCount} import(s) remaining in queue`)
}

while (importHints.length > 0) {
const m: string = importHints.pop() as string
if (m && m in files) continue
if (m && m in files) {
console.log(`[Compiler] ⏭️ Skipping "${m}" - already loaded`)
continue
}

if (this.handleImportCall) {
// Try to use the ImportResolver first, fall back to legacy handleImportCall
if (this.currentResolver) {
const position = remainingCount - importHints.length
console.log(`[Compiler] 🔍 [${position}/${remainingCount}] Resolving import via ImportResolver: "${m}"`)

this.currentResolver.resolveAndSave(m)
.then(content => {
console.log(`[Compiler] ✅ [${position}/${remainingCount}] Successfully resolved: "${m}" (${content?.length || 0} bytes)`)
files[m] = { content }
console.log(`[Compiler] � Recursively calling gatherImports for remaining ${importHints.length} import(s)`)
this.gatherImports(files, importHints, cb)
})
.catch(err => {
console.log(`[Compiler] ❌ [${position}/${remainingCount}] Failed to resolve: "${m}"`)
// Format error message to match handleImportCall pattern
const errorMessage = err && typeof err === 'object' && err.message
? err.message
: (typeof err === 'string' ? err : String(err))
console.log(`[Compiler] ❌ Error details:`, errorMessage)
if (cb) cb(errorMessage)
})
return
} else if (this.handleImportCall) {
const position = remainingCount - importHints.length
console.log(`[Compiler] �🔍 [${position}/${remainingCount}] Resolving import via legacy callback: "${m}"`)

this.handleImportCall(m, (err, content: string) => {
if (err && cb) cb(err)
else {
if (err) {
console.log(`[Compiler] ❌ [${position}/${remainingCount}] Failed to resolve: "${m}" - Error: ${err}`)
if (cb) cb(err)
} else {
console.log(`[Compiler] ✅ [${position}/${remainingCount}] Successfully resolved: "${m}" (${content?.length || 0} bytes)`)
files[m] = { content }

console.log(`[Compiler] 🔄 Recursively calling gatherImports for remaining ${importHints.length} import(s)`)
this.gatherImports(files, importHints, cb)
}
})
}
return
}
console.log(`[Compiler] ✨ All imports resolved! Total files: ${Object.keys(files).length}`)

// Don't clean up resolver here - it needs to survive across multiple compilation rounds
// The resolver will be cleaned up in onCompilationFinished when compilation truly completes

if (cb) { cb(null, { sources: files }) }
}



/**
* @dev Truncate version string
* @param version version
Expand Down
23 changes: 23 additions & 0 deletions libs/remix-solidity/src/compiler/import-resolver-interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Interface for import resolution
* Allows the Compiler to remain agnostic about how imports are resolved
*/
export interface IImportResolver {
/**
* Resolve an import path and return its content
* @param url - The import path to resolve
* @returns Promise resolving to the file content
*/
resolveAndSave(url: string): Promise<string>

/**
* Save the current compilation's resolutions to persistent storage
* Called after successful compilation
*/
saveResolutionsToIndex(): Promise<void>

/**
* Get the target file for this compilation
*/
getTargetFile(): string
}
Loading