Skip to content

Commit 94cac74

Browse files
authored
fix(worldgen): correctly resolve remappings when going through the inheritance chain (#3791)
1 parent 122945e commit 94cac74

6 files changed

Lines changed: 85 additions & 9 deletions

File tree

.changeset/hip-garlics-ring.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@latticexyz/common": patch
3+
"@latticexyz/world": patch
4+
---
5+
6+
Correctly resolve remappings when going through the inheritance chain during worldgen.

packages/common/src/codegen/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from "./formatAndWrite";
44
export * from "./parseSystem";
55
export * from "./resolveInheritedSymbols";
66
export * from "./applyTypeQualifiers";
7+
export * from "./resolveRemapping";

packages/common/src/codegen/utils/resolveInheritedSymbols.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import path from "node:path";
55
import { findContractNode } from "./findContractNode";
66
import { QualifiedSymbol } from "./contractToInterface";
77
import { findSymbolImport } from "./findSymbolImport";
8+
import { resolveRemapping } from "./resolveRemapping";
89

910
interface InheritanceInfo {
1011
baseContracts: string[];
@@ -22,17 +23,20 @@ interface InheritanceInfo {
2223
export async function createInheritanceResolver(
2324
contractPath: string,
2425
contractName: string,
26+
rootDir: string,
27+
remappings: string[] = [],
2528
): Promise<(symbol: string) => QualifiedSymbol | undefined> {
2629
const resolvedContracts = new Map<string, InheritanceInfo>();
2730
const visitedPaths = new Set<string>();
2831

2932
async function parseContract(filePath: string, targetContractName?: string): Promise<InheritanceInfo | undefined> {
3033
// Prevent infinite recursion
3134
const normalizedPath = path.resolve(filePath);
32-
if (visitedPaths.has(normalizedPath)) {
35+
const visitKey = targetContractName ? `${normalizedPath}:${targetContractName}` : normalizedPath;
36+
if (visitedPaths.has(visitKey)) {
3337
return undefined;
3438
}
35-
visitedPaths.add(normalizedPath);
39+
visitedPaths.add(visitKey);
3640

3741
try {
3842
const source = await readFile(filePath, "utf8");
@@ -80,7 +84,7 @@ export async function createInheritanceResolver(
8084
}
8185
}
8286

83-
// Extract symbols defined in this contract
87+
// Extract symbols defined in this contract/interface
8488
visit(contractNode, {
8589
StructDefinition(node) {
8690
if (node.name) {
@@ -109,11 +113,11 @@ export async function createInheritanceResolver(
109113
// Store the original import path
110114
info.baseContractImports.set(baseName, importInfo.path);
111115

112-
const importPath = importInfo.path.startsWith(".")
116+
const resolvedPath = importInfo.path.startsWith(".")
113117
? path.resolve(path.dirname(filePath), importInfo.path)
114-
: importInfo.path;
118+
: resolveRemapping(importInfo.path, remappings, rootDir);
115119

116-
await parseContract(importPath, baseName);
120+
await parseContract(resolvedPath, baseName);
117121
}
118122
}
119123
}
@@ -157,8 +161,24 @@ export async function createInheritanceResolver(
157161
const baseInfo = resolvedContracts.get(baseName);
158162
if (baseInfo?.symbols.has(symbol)) {
159163
// Found the symbol in a base contract
160-
// Get the import path from the main contract to this base contract
161-
const importPath = mainInfo?.baseContractImports.get(baseName) || `./${baseName}.sol`;
164+
// Find the import path for this base contract
165+
let importPath: string | undefined;
166+
167+
for (const [, info] of resolvedContracts) {
168+
const baseImportPath = info.baseContractImports.get(baseName);
169+
if (baseImportPath) {
170+
importPath = baseImportPath;
171+
break;
172+
}
173+
}
174+
175+
// If we still don't have an import path, throw an error
176+
if (!importPath) {
177+
throw new Error(
178+
`Could not find import path for base contract "${baseName}" which defines "${symbol}". ` +
179+
`Make sure "${baseName}" is properly imported in your contract or its dependencies.`,
180+
);
181+
}
162182

163183
return {
164184
symbol,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import path from "path";
2+
3+
/**
4+
* Resolves an import path using forge remappings
5+
* @param importPath The import path to resolve (e.g., "@latticexyz/store/src/Store.sol")
6+
* @param remappings Array of forge remapping strings (e.g., ["@latticexyz/=node_modules/@latticexyz/"])
7+
* @param rootDir The root directory to resolve relative to
8+
* @returns The resolved file path, or the original path if no remapping applies
9+
*/
10+
export function resolveRemapping(importPath: string, remappings: string[], rootDir: string): string {
11+
// Parse remappings into { from, to } objects
12+
const parsedRemappings = remappings.map((remapping) => {
13+
const [from, to] = remapping.split("=");
14+
return { from, to };
15+
});
16+
17+
// Sort by length descending to match longest prefix first
18+
parsedRemappings.sort((a, b) => b.from.length - a.from.length);
19+
20+
// Find the first matching remapping
21+
for (const { from, to } of parsedRemappings) {
22+
if (importPath.startsWith(from)) {
23+
// Replace the prefix with the mapped path
24+
const resolvedPath = importPath.replace(from, to);
25+
// Resolve to absolute path
26+
return path.resolve(rootDir, resolvedPath);
27+
}
28+
}
29+
30+
// No remapping found, return original path
31+
return importPath;
32+
}

packages/common/src/foundry/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,12 @@ export async function getRpcUrl(profile?: string): Promise<string> {
7676
"http://127.0.0.1:8545"
7777
);
7878
}
79+
80+
/**
81+
* Get the remappings from forge config.
82+
* @param profile The foundry profile to use
83+
* @returns Array of remapping strings (e.g., ["@latticexyz/=node_modules/@latticexyz/"])
84+
*/
85+
export async function getRemappings(profile?: string): Promise<string[]> {
86+
return (await getForgeConfig(profile)).remappings;
87+
}

packages/world/ts/node/render-solidity/worldgen.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
applyTypeQualifiers,
88
type ImportDatum,
99
} from "@latticexyz/common/codegen";
10+
import { getRemappings } from "@latticexyz/common/foundry";
1011
import { renderSystemInterface } from "./renderSystemInterface";
1112
import { renderWorldInterface } from "./renderWorldInterface";
1213
import { renderSystemLibrary } from "./renderSystemLibrary";
@@ -23,6 +24,8 @@ export async function worldgen({
2324
config: WorldConfig;
2425
clean?: boolean;
2526
}) {
27+
// Get forge remappings for resolving npm packages
28+
const remappings = await getRemappings();
2629
const worldgenOutDir = path.join(
2730
rootDir,
2831
config.sourceDirectory,
@@ -88,7 +91,12 @@ export async function worldgen({
8891
const source = await fs.readFile(path.join(rootDir, system.sourcePath), "utf8");
8992

9093
// Create inheritance resolver for this system
91-
const findInheritedSymbol = await createInheritanceResolver(path.join(rootDir, system.sourcePath), system.label);
94+
const findInheritedSymbol = await createInheritanceResolver(
95+
path.join(rootDir, system.sourcePath),
96+
system.label,
97+
rootDir,
98+
remappings,
99+
);
92100

93101
// get external functions from a contract
94102
let functions, errors, symbolImports, qualifiedSymbols;

0 commit comments

Comments
 (0)