|
| 1 | +# Context-Aware Dependency Resolution Strategy |
| 2 | + |
| 3 | +## The Problem |
| 4 | + |
| 5 | +When compiling Solidity contracts with npm package imports, the Solidity compiler provides a "missing imports" callback that only gives us: |
| 6 | +- ❌ The missing import path (e.g., `@chainlink/contracts/src/v0.8/token/IERC20.sol`) |
| 7 | +- ❌ NO information about which file requested it |
| 8 | + |
| 9 | +This creates ambiguity when: |
| 10 | +1. Multiple parent packages depend on different versions of the same package |
| 11 | +2. We need to resolve unversioned imports based on their parent's `package.json` |
| 12 | + |
| 13 | +### Real-World Example |
| 14 | + |
| 15 | +```solidity |
| 16 | +// MyContract.sol |
| 17 | +import "@chainlink/contracts-ccip@1.6.1/src/v0.8/ccip/Router.sol"; |
| 18 | +import "@chainlink/contracts-ccip@1.6.2/src/v0.8/ccip/libraries/Client.sol"; |
| 19 | +``` |
| 20 | + |
| 21 | +Where: |
| 22 | +- `contracts-ccip@1.6.1/package.json` → `"@chainlink/contracts": "^1.4.0"` |
| 23 | +- `contracts-ccip@1.6.2/package.json` → `"@chainlink/contracts": "^1.5.0"` |
| 24 | + |
| 25 | +When `Router.sol` imports `@chainlink/contracts/...` (unversioned), should we resolve to 1.4.0 or 1.5.0? |
| 26 | + |
| 27 | +**Old approach:** Use LIFO (most recent parent) → **WRONG!** Might pick the wrong version |
| 28 | +**New approach:** Track which file requests what → **CORRECT!** Use the requesting file's package context |
| 29 | + |
| 30 | +--- |
| 31 | + |
| 32 | +## The Solution: Pre-Compilation Dependency Tree Builder |
| 33 | + |
| 34 | +Instead of relying on the compiler's missing imports callback, we **build our own dependency tree BEFORE compilation**. |
| 35 | + |
| 36 | +### Architecture |
| 37 | + |
| 38 | +``` |
| 39 | +┌─────────────────────────────────────────────────────────────────┐ |
| 40 | +│ DependencyResolver │ |
| 41 | +│ (Pre-compilation dependency tree builder) │ |
| 42 | +│ │ |
| 43 | +│ 1. Start from entry file (e.g., MyContract.sol) │ |
| 44 | +│ 2. Fetch content │ |
| 45 | +│ 3. Extract imports using regex │ |
| 46 | +│ 4. For each import: │ |
| 47 | +│ a. Track: "File X requests import Y" │ |
| 48 | +│ b. Determine package context of File X │ |
| 49 | +│ c. Tell ImportResolver: "Use context of File X" │ |
| 50 | +│ d. Resolve import Y with full context │ |
| 51 | +│ e. Recursively process imported file │ |
| 52 | +│ 5. Build complete source bundle │ |
| 53 | +│ 6. Pass to Solidity compiler │ |
| 54 | +└─────────────────────────────────────────────────────────────────┘ |
| 55 | + ↓ |
| 56 | +┌─────────────────────────────────────────────────────────────────┐ |
| 57 | +│ ImportResolver │ |
| 58 | +│ (Context-aware version resolution) │ |
| 59 | +│ │ |
| 60 | +│ Priority waterfall: │ |
| 61 | +│ 1. Workspace resolutions (package.json) │ |
| 62 | +│ 2. Parent package dependencies ← NOW CONTEXT-AWARE! │ |
| 63 | +│ 3. Lock files (yarn.lock / package-lock.json) │ |
| 64 | +│ 4. NPM registry (fallback) │ |
| 65 | +│ │ |
| 66 | +│ New method: setPackageContext(packageContext) │ |
| 67 | +│ - Called by DependencyResolver before each resolution │ |
| 68 | +│ - Tells resolver: "I'm resolving from within package X@Y" │ |
| 69 | +│ - findParentPackageContext() uses this for accurate lookup │ |
| 70 | +└─────────────────────────────────────────────────────────────────┘ |
| 71 | +``` |
| 72 | + |
| 73 | +--- |
| 74 | + |
| 75 | +## Key Components |
| 76 | + |
| 77 | +### 1. **DependencyResolver** (NEW!) |
| 78 | +`libs/remix-solidity/src/compiler/dependency-resolver.ts` |
| 79 | + |
| 80 | +**Purpose:** Pre-compilation import tree walker |
| 81 | + |
| 82 | +**Responsibilities:** |
| 83 | +- Walk the import graph manually (before compilation) |
| 84 | +- Track: `File A → imports B` (the missing context!) |
| 85 | +- Extract package context from file paths |
| 86 | +- Set context before resolving each import |
| 87 | +- Build complete source bundle |
| 88 | +- Provide compiler-ready input |
| 89 | + |
| 90 | +**Key Methods:** |
| 91 | +- `buildDependencyTree(entryFile)` - Main entry point |
| 92 | +- `processFile(importPath, requestingFile, packageContext)` - Recursive import processor |
| 93 | +- `extractImports(content)` - Regex-based import extraction |
| 94 | +- `extractPackageContext(path)` - Extract `package@version` from path |
| 95 | +- `toCompilerInput()` - Convert to Solidity compiler format |
| 96 | + |
| 97 | +### 2. **ImportResolver** (ENHANCED!) |
| 98 | +`libs/remix-solidity/src/compiler/import-resolver.ts` |
| 99 | + |
| 100 | +**Purpose:** Context-aware version resolution |
| 101 | + |
| 102 | +**New Methods:** |
| 103 | +- `setPackageContext(packageContext)` - Set explicit resolution context |
| 104 | +- `getResolution(originalImport)` - Get resolved path for import |
| 105 | + |
| 106 | +**Enhanced Methods:** |
| 107 | +- `findParentPackageContext()` - Now checks explicit context first |
| 108 | +- All existing resolution logic remains unchanged |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +## How It Works: Step-by-Step |
| 113 | + |
| 114 | +### Scenario: Resolving `@chainlink/contracts` from two different parent versions |
| 115 | + |
| 116 | +``` |
| 117 | +Step 1: DependencyResolver starts with MyContract.sol |
| 118 | + └─ Extracts imports: |
| 119 | + - "@chainlink/contracts-ccip@1.6.1/src/v0.8/ccip/Router.sol" |
| 120 | + - "@chainlink/contracts-ccip@1.6.2/src/v0.8/ccip/libraries/Client.sol" |
| 121 | +
|
| 122 | +Step 2: Process Router.sol |
| 123 | + └─ Package context: "@chainlink/contracts-ccip@1.6.1" |
| 124 | + └─ Set context: resolver.setPackageContext("@chainlink/contracts-ccip@1.6.1") |
| 125 | + └─ Fetch content |
| 126 | + └─ Extract imports: "@chainlink/contracts/src/v0.8/token/IERC20.sol" |
| 127 | + |
| 128 | +Step 3: Resolve IERC20.sol (requested by Router.sol) |
| 129 | + └─ Current context: "@chainlink/contracts-ccip@1.6.1" |
| 130 | + └─ ImportResolver checks parent deps: contracts-ccip@1.6.1 → contracts@1.4.0 |
| 131 | + └─ Resolves to: "@chainlink/contracts@1.4.0/src/v0.8/token/IERC20.sol" ✅ |
| 132 | +
|
| 133 | +Step 4: Process Client.sol |
| 134 | + └─ Package context: "@chainlink/contracts-ccip@1.6.2" |
| 135 | + └─ Set context: resolver.setPackageContext("@chainlink/contracts-ccip@1.6.2") |
| 136 | + └─ Fetch content |
| 137 | + └─ Extract imports: "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol" |
| 138 | +
|
| 139 | +Step 5: Resolve OwnerIsCreator.sol (requested by Client.sol) |
| 140 | + └─ Current context: "@chainlink/contracts-ccip@1.6.2" |
| 141 | + └─ ImportResolver checks parent deps: contracts-ccip@1.6.2 → contracts@1.5.0 |
| 142 | + └─ Resolves to: "@chainlink/contracts@1.5.0/src/v0.8/shared/access/OwnerIsCreator.sol" ✅ |
| 143 | +
|
| 144 | +Step 6: Build source bundle |
| 145 | + └─ MyContract.sol |
| 146 | + └─ @chainlink/contracts-ccip@1.6.1/src/v0.8/ccip/Router.sol |
| 147 | + └─ @chainlink/contracts@1.4.0/src/v0.8/token/IERC20.sol |
| 148 | + └─ @chainlink/contracts-ccip@1.6.2/src/v0.8/ccip/libraries/Client.sol |
| 149 | + └─ @chainlink/contracts@1.5.0/src/v0.8/shared/access/OwnerIsCreator.sol |
| 150 | +
|
| 151 | +Step 7: Pass to Solidity compiler |
| 152 | + └─ Compilation succeeds! ✅ |
| 153 | + └─ No duplicate declarations (different files from different versions) |
| 154 | +``` |
| 155 | + |
| 156 | +--- |
| 157 | + |
| 158 | +## Migration Guide |
| 159 | + |
| 160 | +### Old Approach (Compiler Callback) |
| 161 | +```typescript |
| 162 | +// Compiler calls missing imports callback |
| 163 | +compiler.compile(sources, { |
| 164 | + import: async (path: string) => { |
| 165 | + // ❌ We don't know which file requested this import! |
| 166 | + const content = await importResolver.resolveAndSave(path) |
| 167 | + return { contents: content } |
| 168 | + } |
| 169 | +}) |
| 170 | +``` |
| 171 | + |
| 172 | +### New Approach (Pre-Compilation Builder) |
| 173 | +```typescript |
| 174 | +import { DependencyResolver } from './dependency-resolver' |
| 175 | + |
| 176 | +// 1. Build dependency tree BEFORE compilation |
| 177 | +const depResolver = new DependencyResolver(pluginApi, entryFile) |
| 178 | +const sourceBundle = await depResolver.buildDependencyTree(entryFile) |
| 179 | + |
| 180 | +// 2. Get compiler-ready input |
| 181 | +const compilerInput = depResolver.toCompilerInput() |
| 182 | + |
| 183 | +// 3. Compile with complete source bundle (no missing imports!) |
| 184 | +const output = await compiler.compile({ |
| 185 | + sources: compilerInput, |
| 186 | + settings: { ... } |
| 187 | +}) |
| 188 | +``` |
| 189 | + |
| 190 | +--- |
| 191 | + |
| 192 | +## Benefits |
| 193 | + |
| 194 | +1. ✅ **Context-Aware Resolution** |
| 195 | + - Know exactly which file requests each import |
| 196 | + - Use the requesting file's package context |
| 197 | + - Accurate parent dependency resolution |
| 198 | + |
| 199 | +2. ✅ **Multi-Version Support** |
| 200 | + - Different parent packages can use different versions of the same dependency |
| 201 | + - No conflicts as long as they import different files |
| 202 | + - Compiler receives the correct version for each import |
| 203 | + |
| 204 | +3. ✅ **Better Error Messages** |
| 205 | + - Can warn when multiple parent packages conflict |
| 206 | + - Show user exactly which file needs which version |
| 207 | + - Suggest actionable solutions |
| 208 | + |
| 209 | +4. ✅ **Predictable Behavior** |
| 210 | + - No LIFO heuristics (which might be wrong) |
| 211 | + - Deterministic resolution based on actual package dependencies |
| 212 | + - Same result every time |
| 213 | + |
| 214 | +5. ✅ **Full Import Graph Visibility** |
| 215 | + - Track complete dependency tree |
| 216 | + - Debug import issues easily |
| 217 | + - Understand what the compiler will receive |
| 218 | + |
| 219 | +--- |
| 220 | + |
| 221 | +## Edge Cases Handled |
| 222 | + |
| 223 | +### Case 1: Two parent packages, same child dependency, different versions |
| 224 | +**Solution:** Context-aware resolution uses the correct parent's package.json |
| 225 | + |
| 226 | +### Case 2: Circular imports |
| 227 | +**Solution:** `processedFiles` Set prevents infinite loops |
| 228 | + |
| 229 | +### Case 3: Missing files |
| 230 | +**Solution:** Graceful error handling, continues processing other imports |
| 231 | + |
| 232 | +### Case 4: Workspace overrides |
| 233 | +**Solution:** Priority 1 in resolution waterfall (overrides everything) |
| 234 | + |
| 235 | +--- |
| 236 | + |
| 237 | +## Future Enhancements |
| 238 | + |
| 239 | +1. **Parallel Processing** |
| 240 | + - Process independent imports concurrently |
| 241 | + - Faster build times for large projects |
| 242 | + |
| 243 | +2. **Caching** |
| 244 | + - Cache processed files across compilations |
| 245 | + - Only re-process changed files |
| 246 | + |
| 247 | +3. **Conflict Detection** |
| 248 | + - Warn when same file imported from multiple versions |
| 249 | + - Suggest refactoring strategies |
| 250 | + |
| 251 | +4. **Visualization** |
| 252 | + - Generate import graph visualizations |
| 253 | + - Show dependency tree in IDE |
| 254 | + |
| 255 | +--- |
| 256 | + |
| 257 | +## Testing |
| 258 | + |
| 259 | +See `importResolver.test.ts` for test cases including: |
| 260 | +- ✅ Basic resolution |
| 261 | +- ✅ Explicit versioned imports |
| 262 | +- ✅ Parent dependency resolution |
| 263 | +- ✅ Chainlink CCIP scenario (multi-parent) |
| 264 | +- ✅ Workspace resolutions |
| 265 | +- ✅ Lock file versions |
| 266 | + |
| 267 | +--- |
| 268 | + |
| 269 | +## Files Changed |
| 270 | + |
| 271 | +1. **NEW:** `libs/remix-solidity/src/compiler/dependency-resolver.ts` |
| 272 | + - Pre-compilation dependency tree builder |
| 273 | + |
| 274 | +2. **ENHANCED:** `libs/remix-solidity/src/compiler/import-resolver.ts` |
| 275 | + - Added `setPackageContext()` method |
| 276 | + - Enhanced `findParentPackageContext()` to check explicit context |
| 277 | + - Added `getResolution()` method |
| 278 | + - Added conflict warning for multi-parent dependencies |
| 279 | + |
| 280 | +3. **NEW:** `libs/remix-solidity/src/compiler/dependency-resolver.example.ts` |
| 281 | + - Example usage documentation |
| 282 | + |
| 283 | +--- |
| 284 | + |
| 285 | +## Summary |
| 286 | + |
| 287 | +The new **DependencyResolver** gives us the missing piece: **which file requests which import**. |
| 288 | + |
| 289 | +By building the dependency tree ourselves (instead of relying on the compiler), we can: |
| 290 | +- Track the full import graph |
| 291 | +- Resolve imports with complete context |
| 292 | +- Support multiple versions of parent packages |
| 293 | +- Provide better error messages |
| 294 | +- Ensure deterministic, predictable behavior |
| 295 | + |
| 296 | +This is a **game-changer** for complex dependency scenarios! 🎉 |
0 commit comments