locklift
Version:
Node JS framework for working with Ever contracts. Inspired by Truffle and Hardhat. Helps you to build, test, run and maintain your smart contracts.
216 lines (199 loc) • 7.57 kB
text/typescript
import fs from "fs-extra";
import path from "path";
import { tryToGetNodeModules } from "../cli/builder/utils";
import { tryToGetFileChangeTime } from "./utils";
import chalk from "chalk";
import { defer, from, lastValueFrom, map, mergeMap, tap, toArray } from "rxjs";
import { CacheRecord } from "./types";
import _ from "lodash";
import { BuilderConfig } from "../cli/builder";
const cacheFolder = path.join(".cache/build.json");
const importMatcher = /^\s*import\s*(?:{[^}]+}\s*from\s*)?["']([^"']+\.t?sol)["']\s*;/gm;
const artifactsExtensions = [".tvc", ".abi.json", ".code"];
export class BuildCache {
private prevCache: CacheRecord;
private currentCache: CacheRecord = {};
constructor(
private readonly contracts: string[],
isForce: boolean,
private readonly buildFolder: string,
private readonly compilerConfig: BuilderConfig,
) {
fs.ensureFileSync(cacheFolder);
this.prevCache = isForce ? [] : fs.readJSONSync(cacheFolder, { throws: false }) || [];
if (JSON.stringify(this.prevCache.compilerSettings) !== JSON.stringify(this.compilerConfig)) {
this.clearCache();
}
}
getBuiltContracts() {
const files = fs.readdirSync(this.buildFolder);
return _(files)
.groupBy(el => el.split(".")[0])
.entries()
.filter(([, files]) => artifactsExtensions.every(ext => files.some(file => file.endsWith(ext))))
.map(([contractName]) => contractName)
.value();
}
async buildTree() {
const builtContracts = this.getBuiltContracts();
const { contractsMap, contractsWithImports } = await this.findContractsAndImports(this.contracts);
Array.from(contractsMap.keys())
.filter(el => !builtContracts.includes(path.basename(el).split(".")[0]))
.map(pathToContract => pathToContract)
.forEach(el => this.removeRecordFromCache(el));
const uniqFiles = this.getUniqueFiles(contractsWithImports);
const filesWithModTime = this.applyModTime(uniqFiles);
this.currentCache = filesWithModTime;
const updatedOrNewFiles = this.getUpdatedOrNewFiles(filesWithModTime, this.prevCache);
const importToImportersMap = contractsWithImports.reduce((acc, current) => {
current.imports.forEach(imp => {
acc[imp.path] = acc[imp.path] ? [...acc[imp.path], current.path] : [current.path];
});
return acc;
}, {} as Record<string, string[]>);
const printArr = [] as Array<Print>;
return findFilesForBuildRecursive(updatedOrNewFiles, importToImportersMap, contractsMap, printArr);
}
async findContractsAndImports(contracts: string[]) {
const pathToNodeModules = tryToGetNodeModules();
const contractsMap = new Map<string, boolean>();
const contractsWithImports = await lastValueFrom(
from(contracts).pipe(
mergeMap(contractPath =>
from(
fs.readFile(contractPath, {
encoding: "utf-8",
}),
).pipe(
tap(contractFile => {
if (new RegExp(/^\s*contract [A-Za-z0-9_]+\s*(is\s+[A-Za-z0-9_,\s]+)*\{/gm).test(contractFile)) {
contractsMap.set(contractPath, true);
}
}),
mergeMap(contractFile => {
return from(Array.from(contractFile.matchAll(importMatcher))).pipe(
map(el => el[1]),
mergeMap(imp =>
defer(async () => {
const localImportPath = path.join(contractPath, "..", imp);
const localFileChangeTime = await tryToGetFileChangeTime(localImportPath);
if (localFileChangeTime) {
return {
path: localImportPath,
modificationTime: localFileChangeTime,
};
}
const nodeModulesImportPath = path.join(pathToNodeModules!, imp);
const nodeModulesFileChangeTime = await tryToGetFileChangeTime(nodeModulesImportPath);
if (nodeModulesFileChangeTime) {
return {
path: nodeModulesImportPath,
modificationTime: nodeModulesFileChangeTime,
};
}
throw new Error(`Can't find import ${imp} for file ${contractPath}`);
}),
),
toArray(),
);
}),
map(imports => ({ path: contractPath, imports })),
),
),
toArray(),
),
);
return { contractsWithImports, contractsMap };
}
getUniqueFiles(contractsWithImports: Array<{ path: string; imports: Array<{ path: string }> }>) {
const uniqFiles = new Set<string>();
[
...contractsWithImports.map(el => el.path),
...contractsWithImports.flatMap(el => el.imports.map(el => el.path)),
].forEach(el => uniqFiles.add(el));
return Array.from(uniqFiles);
}
getUpdatedOrNewFiles(filesWithModTime: CacheRecord, cache: CacheRecord) {
return Object.entries(filesWithModTime)
.filter(([filePath, { modificationTime }]) => {
const prevFile = cache[filePath];
if (!prevFile) {
return true;
}
return prevFile.modificationTime !== modificationTime;
})
.map(([filePath]) => filePath);
}
applyModTime(files: string[]): CacheRecord {
return files.reduce((acc, el) => {
return { ...acc, [el]: { modificationTime: fs.statSync(el).mtime.getTime() } };
}, {} as CacheRecord);
}
applyCurrentCache() {
fs.writeJSONSync(
cacheFolder,
{ ...this.currentCache, compilerSettings: this.compilerConfig },
{
spaces: 4,
},
);
}
applyOldCache() {
fs.writeJSONSync(cacheFolder, this.prevCache, {
spaces: 4,
});
}
clearCache() {
fs.writeJSONSync(cacheFolder, [], {
spaces: 4,
});
this.prevCache = {};
}
removeRecordFromCache(filePath: string) {
delete this.prevCache[filePath];
this.applyOldCache();
}
static clearCache() {
fs.rmSync(cacheFolder, { recursive: true });
}
}
type Print = { filePath: string; subDep: Array<Print> };
const recursivePrint = (printArr: Array<Print>, level = 0, selectedContracts: Array<string>) => {
printArr.forEach(el => {
console.log(
`${" ".repeat(level)}${selectedContracts.includes(el.filePath) ? chalk.blueBright(el.filePath) : el.filePath}`,
);
recursivePrint(el.subDep, level + 1, selectedContracts);
});
};
const findFilesForBuildRecursive = (
updatedOrNewFiles: string[],
importToFileMap: Record<string, Array<string>>,
contractsMap: Map<string, boolean>,
printArr: Array<Print>,
visitedMap: Map<string, boolean> = new Map(),
): Array<string> => {
return updatedOrNewFiles.reduce((acc, filePath) => {
const importRecords = importToFileMap[filePath];
const prevVisited = new Map(visitedMap);
if (visitedMap.get(filePath)) {
return acc;
}
visitedMap.set(filePath, true);
/// debug
const newPrintArr = [] as Array<Print>;
printArr.push({ filePath, subDep: newPrintArr });
if (!importRecords || importRecords.length === 0) {
acc.push(filePath);
return acc;
}
const notVisitedFiles = importRecords.filter(el => !prevVisited.get(el));
if (contractsMap.get(filePath)) {
acc.push(filePath);
}
return [
...acc,
...findFilesForBuildRecursive(notVisitedFiles, importToFileMap, contractsMap, newPrintArr, visitedMap),
];
}, [] as string[]);
};