@primitivefi/hardhat-dodoc
Version:
Zero-config Hardhat plugin to generate documentation for all your Solidity contracts
217 lines • 13.1 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* eslint-disable guard-for-in, max-len, no-await-in-loop, no-restricted-syntax */
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const config_1 = require("hardhat/config");
const task_names_1 = require("hardhat/builtin-tasks/task-names");
const Sqrl = __importStar(require("squirrelly"));
const abiDecoder_1 = require("./abiDecoder");
require("./type-extensions");
(0, config_1.extendConfig)((config, userConfig) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
// eslint-disable-next-line no-param-reassign
config.dodoc = {
include: ((_a = userConfig.dodoc) === null || _a === void 0 ? void 0 : _a.include) || [],
exclude: ((_b = userConfig.dodoc) === null || _b === void 0 ? void 0 : _b.exclude) || [],
runOnCompile: ((_c = userConfig.dodoc) === null || _c === void 0 ? void 0 : _c.runOnCompile) !== undefined ? (_d = userConfig.dodoc) === null || _d === void 0 ? void 0 : _d.runOnCompile : true,
debugMode: ((_e = userConfig.dodoc) === null || _e === void 0 ? void 0 : _e.debugMode) || false,
outputDir: ((_f = userConfig.dodoc) === null || _f === void 0 ? void 0 : _f.outputDir) || './docs',
templatePath: ((_g = userConfig.dodoc) === null || _g === void 0 ? void 0 : _g.templatePath) || path_1.default.join(__dirname, './template.sqrl'),
keepFileStructure: (_j = (_h = userConfig.dodoc) === null || _h === void 0 ? void 0 : _h.keepFileStructure) !== null && _j !== void 0 ? _j : true,
freshOutput: (_l = (_k = userConfig.dodoc) === null || _k === void 0 ? void 0 : _k.freshOutput) !== null && _l !== void 0 ? _l : true,
};
});
async function generateDocumentation(hre) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
const config = hre.config.dodoc;
const docs = [];
const qualifiedNames = await hre.artifacts.getAllFullyQualifiedNames();
const filteredQualifiedNames = qualifiedNames.filter((filePath) => {
// Checks if the documentation has to be generated for this contract
const includesPath = config.include.some((str) => filePath.includes(str));
const excludesPath = config.exclude.some((str) => filePath.includes(str));
return (config.include.length === 0 || includesPath) && !excludesPath;
});
// Loops through all the qualified names to get all the compiled contracts
const sourcesPath = hre.config.paths.sources.substr(process.cwd().length + 1); // trick to get relative path to files, and trim the first /
for (const qualifiedName of filteredQualifiedNames) {
const [source, name] = qualifiedName.split(':');
const buildInfo = await hre.artifacts.getBuildInfo(qualifiedName);
const info = buildInfo === null || buildInfo === void 0 ? void 0 : buildInfo.output.contracts[source][name];
if (config.debugMode) {
console.log('ABI:\n');
console.log(JSON.stringify(info.abi, null, 4));
console.log('\n\n');
console.log('User doc:\n');
console.log(JSON.stringify(info.userdoc, null, 4));
console.log('\n\n');
console.log('Dev doc:\n');
console.log(JSON.stringify(info.devdoc, null, 4));
}
const doc = Object.assign(Object.assign({}, (0, abiDecoder_1.decodeAbi)(info.abi)), { path: source.substr(sourcesPath.length).split('/').slice(0, -1).join('/') }); // get file path without filename
// Fetches info from userdoc
for (const errorSig in (_a = info.userdoc) === null || _a === void 0 ? void 0 : _a.errors) {
const [errorName] = errorSig.split('(');
const error = (_b = info.userdoc) === null || _b === void 0 ? void 0 : _b.errors[errorSig][0];
if (doc.errors[errorName] !== undefined)
doc.errors[errorName].notice = error === null || error === void 0 ? void 0 : error.notice;
}
for (const eventSig in (_c = info.userdoc) === null || _c === void 0 ? void 0 : _c.events) {
const [eventName] = eventSig.split('(');
const event = (_d = info.userdoc) === null || _d === void 0 ? void 0 : _d.events[eventSig];
if (doc.events[eventName] !== undefined)
doc.events[eventName].notice = event === null || event === void 0 ? void 0 : event.notice;
}
for (const methodSig in (_e = info.userdoc) === null || _e === void 0 ? void 0 : _e.methods) {
// const [methodName] = methodSig.split('(');
const method = (_f = info.userdoc) === null || _f === void 0 ? void 0 : _f.methods[methodSig];
if (doc.methods[methodSig] !== undefined)
doc.methods[methodSig].notice = method === null || method === void 0 ? void 0 : method.notice;
}
// Fetches info from devdoc
for (const errorSig in (_g = info.devdoc) === null || _g === void 0 ? void 0 : _g.errors) {
const [errorName] = errorSig.split('(');
const error = (_h = info.devdoc) === null || _h === void 0 ? void 0 : _h.errors[errorSig][0];
if (doc.errors[errorName] !== undefined)
doc.errors[errorName].details = error === null || error === void 0 ? void 0 : error.details;
for (const param in error === null || error === void 0 ? void 0 : error.params) {
if (doc.errors[errorName].inputs[param])
doc.errors[errorName].inputs[param].description = error === null || error === void 0 ? void 0 : error.params[param];
}
}
for (const eventSig in (_j = info.devdoc) === null || _j === void 0 ? void 0 : _j.events) {
const [eventName] = eventSig.split('(');
const event = (_k = info.devdoc) === null || _k === void 0 ? void 0 : _k.events[eventSig];
if (doc.events[eventName] !== undefined)
doc.events[eventName].details = event === null || event === void 0 ? void 0 : event.details;
for (const param in event === null || event === void 0 ? void 0 : event.params) {
if (doc.events[eventName].inputs[param])
doc.events[eventName].inputs[param].description = event === null || event === void 0 ? void 0 : event.params[param];
}
}
for (const methodSig in (_l = info.devdoc) === null || _l === void 0 ? void 0 : _l.methods) {
const [methodName] = methodSig.split('(');
const method = (_m = info.devdoc) === null || _m === void 0 ? void 0 : _m.methods[methodSig];
if (doc.methods[methodSig] !== undefined && methodName !== 'constructor') {
doc.methods[methodSig].details = method === null || method === void 0 ? void 0 : method.details;
for (const param in method === null || method === void 0 ? void 0 : method.params) {
if (doc.methods[methodSig].inputs[param])
doc.methods[methodSig].inputs[param].description = method === null || method === void 0 ? void 0 : method.params[param];
}
for (const output in method === null || method === void 0 ? void 0 : method.returns) {
if (doc.methods[methodSig].outputs[output])
doc.methods[methodSig].outputs[output].description = method === null || method === void 0 ? void 0 : method.returns[output];
}
}
}
for (const varName in (_o = info.devdoc) === null || _o === void 0 ? void 0 : _o.stateVariables) {
const variable = (_p = info.devdoc) === null || _p === void 0 ? void 0 : _p.stateVariables[varName];
const abiInfo = info.abi.find((a) => a.name === varName);
const varNameWithParams = `${varName}(${(abiInfo === null || abiInfo === void 0 ? void 0 : abiInfo.inputs) ? abiInfo.inputs.map((inp) => inp.type).join(',') : ''})`;
if (doc.methods[varNameWithParams])
doc.methods[varNameWithParams].details = variable === null || variable === void 0 ? void 0 : variable.details;
for (const param in variable === null || variable === void 0 ? void 0 : variable.params) {
if (doc.methods[varNameWithParams].inputs[param])
doc.methods[varNameWithParams].inputs[param].description = variable === null || variable === void 0 ? void 0 : variable.params[param];
}
for (const output in variable === null || variable === void 0 ? void 0 : variable.returns) {
if (doc.methods[varNameWithParams].outputs[output])
doc.methods[varNameWithParams].outputs[output].description = variable === null || variable === void 0 ? void 0 : variable.returns[output];
}
}
// Fetches global info
if ((_q = info.devdoc) === null || _q === void 0 ? void 0 : _q.title)
doc.title = info.devdoc.title;
if ((_r = info.userdoc) === null || _r === void 0 ? void 0 : _r.notice)
doc.notice = info.userdoc.notice;
if ((_s = info.devdoc) === null || _s === void 0 ? void 0 : _s.details)
doc.details = info.devdoc.details;
if ((_t = info.devdoc) === null || _t === void 0 ? void 0 : _t.author)
doc.author = info.devdoc.author;
doc.name = name;
docs.push(doc);
}
try {
await fs_1.default.promises.access(config.outputDir);
if (config.freshOutput) {
await fs_1.default.promises.rm(config.outputDir, {
recursive: true,
});
await fs_1.default.promises.mkdir(config.outputDir);
}
}
catch (e) {
await fs_1.default.promises.mkdir(config.outputDir);
}
const template = await fs_1.default.promises.readFile(config.templatePath, {
encoding: 'utf-8',
});
for (let i = 0; i < docs.length; i += 1) {
const result = Sqrl.render(template, docs[i]);
let docfileName = `${docs[i].name}.md`;
let testFileName = `${docs[i].name}.json`;
if (config.keepFileStructure && docs[i].path !== undefined) {
if (!fs_1.default.existsSync(path_1.default.join(config.outputDir, docs[i].path)))
await fs_1.default.promises.mkdir(path_1.default.join(config.outputDir, docs[i].path), { recursive: true });
docfileName = path_1.default.join(docs[i].path, docfileName);
testFileName = path_1.default.join(docs[i].path, testFileName);
}
await fs_1.default.promises.writeFile(path_1.default.join(config.outputDir, docfileName), result, {
encoding: 'utf-8',
});
if (config.debugMode) {
await fs_1.default.promises.writeFile(path_1.default.join(config.outputDir, testFileName), JSON.stringify(docs[i], null, 4), {
encoding: 'utf-8',
});
}
}
console.log('✅ Generated documentation for', docs.length, docs.length > 1 ? 'contracts' : 'contract');
}
// Custom standalone task
(0, config_1.task)('dodoc', 'Generates NatSpec documentation for the project')
.addFlag('noCompile', 'Prevents compiling before running this task')
.setAction(async (args, hre) => {
if (!args.noCompile) {
await hre.run(task_names_1.TASK_COMPILE, { noDodoc: true });
}
await generateDocumentation(hre);
});
// Overriding task triggered when COMPILE is called
(0, config_1.task)(task_names_1.TASK_COMPILE)
.addFlag('noDodoc', 'Prevents generating NatSpec documentation for the project')
.setAction(async (args, hre, runSuper) => {
// Updates the compiler settings
for (const compiler of hre.config.solidity.compilers) {
compiler.settings.outputSelection['*']['*'].push('devdoc');
compiler.settings.outputSelection['*']['*'].push('userdoc');
}
// Compiles the contracts
await runSuper();
if (hre.config.dodoc.runOnCompile && !args.noDodoc) {
await hre.run('dodoc', { noCompile: true });
}
});
//# sourceMappingURL=index.js.map
;