I see. Since you want to use solc-js
as a library, the recommended approach is to define an import callback function that loads all contracts from disk and includes them in the JSON input. As shown in this example: GitHub - ethereum/solc-js: Javascript bindings for the Solidity compiler. Pay special attention to the findImports
function and how it is used.
However, implementing this can be tricky, depending on your setup. If you’re using Foundry, it typically installs dependencies under ./lib
and applies remappings, while Hardhat relies on node_modules
.
I created a small example that works with both Foundry and Hardhat. I haven’t exhaustively tested it, but I believe it should serve as a helpful guide to achieving what you’re trying to do.
forge init test --no-commit
cd test
forge install OpenZeppelin/openzeppelin-contracts --no-commit
echo "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/" > remappings.txt
astGenerator.js:
const solc = require("solc");
const fs = require("fs");
const path = require("path");
console.log("solc version:", solc.version());
const projectRoot = path.resolve(__dirname);
const remappingsPath = path.join(projectRoot, "remappings.txt");
// Read remappings and store them in an object
const remappings = fs.existsSync(remappingsPath)
? Object.fromEntries(
fs.readFileSync(remappingsPath, "utf8")
.split("\n")
.map(line => line.trim())
.filter(Boolean)
.map(line => line.split("="))
)
: {};
// Remmapings lookups
function resolveWithRemappings(importPath) {
for (const prefix in remappings) {
if (importPath.startsWith(prefix)) {
const resolvedPath = path.resolve(projectRoot, remappings[prefix], importPath.slice(prefix.length));
return { contents: fs.readFileSync(resolvedPath, "utf8") };
}
}
return null;
}
function findImports(importPath) {
console.log(`Resolving import: ${importPath}`);
// Remappings.txt
const remappedImport = resolveWithRemappings(importPath);
if (remappedImport) return remappedImport;
// Node modules
const nodeModulesPath = path.resolve(projectRoot, "node_modules", importPath);
if (fs.existsSync(nodeModulesPath))
return { contents: fs.readFileSync(nodeModulesPath, "utf8") };
// Fallback to relative path
const resolvedPath = path.resolve(projectRoot, importPath);
if (fs.existsSync(resolvedPath))
return { contents: fs.readFileSync(resolvedPath, "utf8") };
return { error: `File not found: ${importPath}` };
}
const filename = "C.sol";
const sourceCode = fs.readFileSync("src/C.sol", "utf8");
// If you want to extract the AST of a single file:
var input = {
language: "Solidity",
sources: {
[filename]: {
content: sourceCode,
},
},
settings: {
outputSelection: {
[filename]: {
"": ["ast"],
},
},
},
};
const output = JSON.parse(solc.compile(JSON.stringify(input), { import: findImports }));
if (output.sources && output.sources[filename]) {
console.log(JSON.stringify(output.sources[filename].ast, null, 2));
} else {
console.log("No AST found for", filename);
}
// If you want to extract the AST of all contracts:
// NOTE: remember to change the AST output selection to "*" in the settings
//Object.keys(output.sources || {}).forEach(contractName => {
// console.log(`AST for ${contractName}:`);
// console.log(JSON.stringify(output.sources[contractName].ast, null, 2));
//});
src/C.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract C is Ownable {
constructor(address _owner) Ownable(_owner) {}
}