commit
e2bb45f0f2
@ -0,0 +1,13 @@ |
|||||||
|
{ |
||||||
|
"presets": [ |
||||||
|
[ |
||||||
|
"@nrwl/react/babel", { |
||||||
|
"runtime": "automatic" |
||||||
|
|
||||||
|
} |
||||||
|
] |
||||||
|
], |
||||||
|
"plugins": [ |
||||||
|
|
||||||
|
] |
||||||
|
} |
@ -1 +0,0 @@ |
|||||||
export * from './app/debugger-api'; |
|
@ -1,32 +1,87 @@ |
|||||||
const nxWebpack = require('@nrwl/react/plugins/webpack') |
const { composePlugins, withNx } = require('@nrwl/webpack') |
||||||
|
const webpack = require('webpack') |
||||||
module.exports = config => { |
const TerserPlugin = require("terser-webpack-plugin") |
||||||
const nxWebpackConfig = nxWebpack(config) |
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin") |
||||||
|
|
||||||
const webpackConfig = { |
|
||||||
...nxWebpackConfig, |
// Nx plugins for webpack.
|
||||||
resolve : { |
module.exports = composePlugins(withNx(), (config) => { |
||||||
...nxWebpackConfig.resolve, |
// Update the webpack config as needed here.
|
||||||
fallback: { |
// e.g. `config.plugins.push(new MyPlugin())`
|
||||||
...nxWebpackConfig.resolve.fallback, |
|
||||||
"crypto": require.resolve("crypto-browserify"), |
// add fallback for node modules
|
||||||
"stream": require.resolve("stream-browserify"), |
config.resolve.fallback = { |
||||||
"http" : require.resolve("stream-http"), |
...config.resolve.fallback, |
||||||
"https" : require.resolve("https-browserify"), |
"crypto": require.resolve("crypto-browserify"), |
||||||
"path" : require.resolve("path-browserify"), |
"stream": require.resolve("stream-browserify"), |
||||||
"module": false, |
"path": require.resolve("path-browserify"), |
||||||
"fs" : false |
"http": require.resolve("stream-http"), |
||||||
}, |
"https": require.resolve("https-browserify"), |
||||||
} |
"constants": require.resolve("constants-browserify"), |
||||||
|
"os": false, //require.resolve("os-browserify/browser"),
|
||||||
|
"timers": false, // require.resolve("timers-browserify"),
|
||||||
|
"zlib": require.resolve("browserify-zlib"), |
||||||
|
"fs": false, |
||||||
|
"module": false, |
||||||
|
"tls": false, |
||||||
|
"net": false, |
||||||
|
"readline": false, |
||||||
|
"child_process": false, |
||||||
|
"buffer": require.resolve("buffer/"), |
||||||
|
"vm": require.resolve('vm-browserify'), |
||||||
} |
} |
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') { |
|
||||||
return { |
// add externals
|
||||||
...webpackConfig, |
config.externals = { |
||||||
mode: 'production', |
...config.externals, |
||||||
devtool: 'source-map', |
solc: 'solc', |
||||||
} |
|
||||||
} else { |
|
||||||
return webpackConfig |
|
||||||
} |
} |
||||||
} |
|
||||||
|
// add public path
|
||||||
|
config.output.publicPath = '/' |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// add copy & provide plugin
|
||||||
|
config.plugins.push( |
||||||
|
new webpack.ProvidePlugin({ |
||||||
|
Buffer: ['buffer', 'Buffer'], |
||||||
|
url: ['url', 'URL'], |
||||||
|
process: 'process/browser', |
||||||
|
}) |
||||||
|
) |
||||||
|
|
||||||
|
// souce-map loader
|
||||||
|
config.module.rules.push({ |
||||||
|
test: /\.js$/, |
||||||
|
use: ["source-map-loader"], |
||||||
|
enforce: "pre" |
||||||
|
}) |
||||||
|
|
||||||
|
config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings
|
||||||
|
|
||||||
|
|
||||||
|
// set minimizer
|
||||||
|
config.optimization.minimizer = [ |
||||||
|
new TerserPlugin({ |
||||||
|
parallel: true, |
||||||
|
terserOptions: { |
||||||
|
ecma: 2015, |
||||||
|
compress: false, |
||||||
|
mangle: false, |
||||||
|
format: { |
||||||
|
comments: false, |
||||||
|
}, |
||||||
|
}, |
||||||
|
extractComments: false, |
||||||
|
}), |
||||||
|
new CssMinimizerPlugin(), |
||||||
|
]; |
||||||
|
|
||||||
|
config.watchOptions = { |
||||||
|
ignored: /node_modules/ |
||||||
|
} |
||||||
|
|
||||||
|
return config; |
||||||
|
}); |
||||||
|
@ -0,0 +1,34 @@ |
|||||||
|
{ |
||||||
|
"extends": [ |
||||||
|
"plugin:@nrwl/nx/react", |
||||||
|
"../../.eslintrc.json" |
||||||
|
], |
||||||
|
"ignorePatterns": [ |
||||||
|
"!**/*" |
||||||
|
], |
||||||
|
"overrides": [ |
||||||
|
{ |
||||||
|
"files": [ |
||||||
|
"*.ts", |
||||||
|
"*.tsx", |
||||||
|
"*.js", |
||||||
|
"*.jsx" |
||||||
|
], |
||||||
|
"rules": {} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"files": [ |
||||||
|
"*.ts", |
||||||
|
"*.tsx" |
||||||
|
], |
||||||
|
"rules": {} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"files": [ |
||||||
|
"*.js", |
||||||
|
"*.jsx" |
||||||
|
], |
||||||
|
"rules": {} |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
{ |
||||||
|
"semi": false, |
||||||
|
"singleQuote": true, |
||||||
|
"trailingComma": "all", |
||||||
|
"printWidth": 120, |
||||||
|
"tabWidth": 2, |
||||||
|
"useTabs": false, |
||||||
|
"arrowParens": "avoid", |
||||||
|
"bracketSpacing": true, |
||||||
|
"jsxBracketSameLine": false, |
||||||
|
"jsxSingleQuote": false, |
||||||
|
"endOfLine": "lf" |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
{ |
||||||
|
"name": "doc-gen", |
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json", |
||||||
|
"sourceRoot": "apps/doc-gen/src", |
||||||
|
"projectType": "application", |
||||||
|
"implicitDependencies": [ |
||||||
|
], |
||||||
|
"targets": { |
||||||
|
"build": { |
||||||
|
"executor": "@nrwl/webpack:webpack", |
||||||
|
"outputs": ["{options.outputPath}"], |
||||||
|
"defaultConfiguration": "development", |
||||||
|
"options": { |
||||||
|
"compiler": "babel", |
||||||
|
"outputPath": "dist/apps/doc-gen", |
||||||
|
"index": "apps/doc-gen/src/index.html", |
||||||
|
"baseHref": "./", |
||||||
|
"main": "apps/doc-gen/src/main.tsx", |
||||||
|
"tsConfig": "apps/doc-gen/tsconfig.app.json", |
||||||
|
"assets": [ |
||||||
|
"apps/doc-gen/src/favicon.ico", |
||||||
|
"apps/doc-gen/src/profile.json" |
||||||
|
], |
||||||
|
"styles": [], |
||||||
|
"scripts": [], |
||||||
|
"webpackConfig": "apps/doc-gen/webpack.config.js" |
||||||
|
}, |
||||||
|
"configurations": { |
||||||
|
"development": { |
||||||
|
}, |
||||||
|
"production": { |
||||||
|
"fileReplacements": [ |
||||||
|
{ |
||||||
|
"replace": "apps/doc-gen/src/environments/environment.ts", |
||||||
|
"with": "apps/doc-gen/src/environments/environment.prod.ts" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
"serve": { |
||||||
|
"executor": "@nrwl/webpack:dev-server", |
||||||
|
"defaultConfiguration": "development", |
||||||
|
"options": { |
||||||
|
"buildTarget": "doc-gen:build", |
||||||
|
"hmr": true, |
||||||
|
"baseHref": "/" |
||||||
|
}, |
||||||
|
"configurations": { |
||||||
|
"development": { |
||||||
|
"buildTarget": "doc-gen:build:development", |
||||||
|
"port": 6003 |
||||||
|
}, |
||||||
|
"production": { |
||||||
|
"buildTarget": "doc-gen:build:production" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
"tags": [] |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
import React, { useState, useEffect } from 'react' |
||||||
|
|
||||||
|
import './App.css' |
||||||
|
import { DocGenClient } from './docgen-client' |
||||||
|
import { Build } from './docgen/site' |
||||||
|
|
||||||
|
export const client = new DocGenClient() |
||||||
|
|
||||||
|
const App = () => { |
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const [themeType, setThemeType] = useState<string>('dark'); |
||||||
|
const [hasBuild, setHasBuild] = useState<boolean>(false); |
||||||
|
const [fileName, setFileName] = useState<string>(''); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const watchThemeSwitch = async () => { |
||||||
|
client.eventEmitter.on('themeChanged', (theme: string) => { |
||||||
|
setThemeType(theme) |
||||||
|
}) |
||||||
|
client.eventEmitter.on('compilationFinished', (build: Build, fileName: string) => { |
||||||
|
setHasBuild(true) |
||||||
|
setFileName(fileName) |
||||||
|
}) |
||||||
|
client.eventEmitter.on('docsGenerated', (docs: string[]) => { |
||||||
|
}) |
||||||
|
} |
||||||
|
watchThemeSwitch() |
||||||
|
}, []) |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="p-3"> |
||||||
|
<h5 className="h-5 mb-3">Compile a Solidity contract and generate its documentation as Markdown. (Right-click on a contract in the File Explorer and select "Generate Docs" from the context menu.).</h5> |
||||||
|
{fileName && <div className="border-bottom border-top px-2 py-3 justify-center align-items-center d-flex"> |
||||||
|
<h6>File: {fileName}</h6> |
||||||
|
</div>} |
||||||
|
{hasBuild && <button className="btn btn-primary btn-block mt-4" onClick={() => client.generateDocs()}>Generate Docs</button>} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default App |
@ -0,0 +1,93 @@ |
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */ |
||||||
|
import { PluginClient } from '@remixproject/plugin' |
||||||
|
import { CompilationResult, SourceWithTarget, customAction } from '@remixproject/plugin-api' |
||||||
|
import { createClient } from '@remixproject/plugin-webview' |
||||||
|
import EventEmitter from 'events' |
||||||
|
import { Config, defaults } from './docgen/config' |
||||||
|
import { Build, buildSite } from './docgen/site' |
||||||
|
import { loadTemplates } from './docgen/templates' |
||||||
|
import { SolcInput, SolcOutput } from 'solidity-ast/solc' |
||||||
|
import { render } from './docgen/render' |
||||||
|
import { normalizeContractPath } from './docgen/utils/normalizeContractPath' |
||||||
|
|
||||||
|
export class DocGenClient extends PluginClient { |
||||||
|
private currentTheme |
||||||
|
public eventEmitter: EventEmitter |
||||||
|
private build: Build |
||||||
|
public docs: string[] = [] |
||||||
|
private fileName: string = '' |
||||||
|
private contractPath: string = '' |
||||||
|
|
||||||
|
constructor() { |
||||||
|
super() |
||||||
|
this.eventEmitter = new EventEmitter() |
||||||
|
this.methods = ['generateDocs', 'openDocs', 'generateDocsCustomAction'] |
||||||
|
createClient(this) |
||||||
|
this.onload().then(async () => { |
||||||
|
await this.setListeners() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
async setListeners() { |
||||||
|
this.currentTheme = await this.call('theme', 'currentTheme') |
||||||
|
|
||||||
|
this.on('theme', 'themeChanged', (theme: any) => { |
||||||
|
this.currentTheme = theme |
||||||
|
this.eventEmitter.emit('themeChanged', this.currentTheme) |
||||||
|
}); |
||||||
|
this.eventEmitter.emit('themeChanged', this.currentTheme) |
||||||
|
|
||||||
|
this.on('solidity', 'compilationFinished', (fileName: string, source: SourceWithTarget, languageVersion: string, data: CompilationResult) => { |
||||||
|
const input: SolcInput = { |
||||||
|
sources: source.sources |
||||||
|
} |
||||||
|
const output: SolcOutput = { |
||||||
|
sources: data.sources as any |
||||||
|
} |
||||||
|
this.build = { |
||||||
|
input: input, |
||||||
|
output: output |
||||||
|
} |
||||||
|
const segmentedPathList = normalizeContractPath(fileName) |
||||||
|
this.fileName = segmentedPathList[segmentedPathList.length - 1] |
||||||
|
this.contractPath = segmentedPathList[0] |
||||||
|
this.eventEmitter.emit('compilationFinished', this.build, this.fileName) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
async generateDocsCustomAction(action: customAction) { |
||||||
|
await this.call('solidity', 'compile', action.path[0]) |
||||||
|
await this.generateDocs() |
||||||
|
} |
||||||
|
|
||||||
|
async docgen(builds: Build[], userConfig?: Config): Promise<void> { |
||||||
|
const config = { ...defaults, ...userConfig } |
||||||
|
config.sourcesDir = this.contractPath !== config.sourcesDir ? this.contractPath : config.sourcesDir |
||||||
|
const templates = await loadTemplates(config.theme, config.root, config.templates) |
||||||
|
const site = buildSite(builds, config, templates.properties ?? {}) |
||||||
|
const renderedSite = render(site, templates, config.collapseNewlines) |
||||||
|
const docs: string[] = [] |
||||||
|
for (const { id, contents } of renderedSite) { |
||||||
|
const temp = `${this.fileName.split('.')[0]}.${id.split('.')[1]}` |
||||||
|
const newFileName = `docs/${temp}` |
||||||
|
await this.call('fileManager', 'setFile', newFileName , contents) |
||||||
|
docs.push(newFileName) |
||||||
|
} |
||||||
|
this.eventEmitter.emit('docsGenerated', docs) |
||||||
|
this.emit('docgen' as any, 'docsGenerated', docs) |
||||||
|
this.docs = docs |
||||||
|
await this.openDocs(docs) |
||||||
|
} |
||||||
|
|
||||||
|
async openDocs(docs: string[]) { |
||||||
|
await this.call('manager', 'activatePlugin', 'doc-viewer') |
||||||
|
await this.call('tabs' as any, 'focus', 'doc-viewer') |
||||||
|
await this.call('doc-viewer' as any, 'viewDocs', docs) |
||||||
|
} |
||||||
|
|
||||||
|
async generateDocs() { |
||||||
|
this.eventEmitter.on('compilationFinished', async (build: Build, fileName: string) => { |
||||||
|
await this.docgen([build]) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
import { VariableDeclaration } from "solidity-ast"; |
||||||
|
|
||||||
|
export function trim(text: string) { |
||||||
|
if (typeof text === 'string') { |
||||||
|
return text.trim(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function joinLines(text?: string) { |
||||||
|
if (typeof text === 'string') { |
||||||
|
return text.replace(/\n+/g, ' '); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Format a variable as its type followed by its name, if available. |
||||||
|
*/ |
||||||
|
export function formatVariable(v: VariableDeclaration): string { |
||||||
|
return [v.typeName?.typeDescriptions.typeString].concat(v.name || []).join(' '); |
||||||
|
} |
||||||
|
|
||||||
|
export const eq = (a: unknown, b: unknown) => a === b; |
||||||
|
|
||||||
|
export const slug = (str) => { |
||||||
|
if (str === undefined) { |
||||||
|
throw new Error('Missing argument'); |
||||||
|
} |
||||||
|
return str.replace(/\W/g, '-'); |
||||||
|
} |
||||||
|
|
||||||
|
export const names = params => params.map(p => p.name).join(', '); |
||||||
|
|
||||||
|
export const typedParams = params => { |
||||||
|
return params?.map(p => `${p.type}${p.indexed ? ' indexed' : ''}${p.name ? ' ' + p.name : ''}`).join(', '); |
||||||
|
}; |
@ -0,0 +1,180 @@ |
|||||||
|
import { EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, ModifierDefinition, ParameterList, StructDefinition, UserDefinedValueTypeDefinition, VariableDeclaration } from 'solidity-ast'; |
||||||
|
import { findAll, isNodeType } from 'solidity-ast/utils'; |
||||||
|
import { NatSpec, parseNatspec } from '../utils/natspec'; |
||||||
|
import { DocItemContext, DOC_ITEM_CONTEXT } from '../site'; |
||||||
|
import { mapValues } from '../utils/map-values'; |
||||||
|
import { DocItem, docItemTypes } from '../doc-item'; |
||||||
|
import { formatVariable, slug } from './helpers'; |
||||||
|
import { PropertyGetter } from '../templates'; |
||||||
|
import { itemType } from '../utils/item-type'; |
||||||
|
|
||||||
|
type TypeDefinition = StructDefinition | EnumDefinition | UserDefinedValueTypeDefinition; |
||||||
|
|
||||||
|
export function type ({ item }: DocItemContext): string { |
||||||
|
return itemType(item); |
||||||
|
} |
||||||
|
|
||||||
|
export function natspec ({ item }: DocItemContext): NatSpec { |
||||||
|
return parseNatspec(item); |
||||||
|
} |
||||||
|
|
||||||
|
export function name({ item }: DocItemContext, original?: unknown): string { |
||||||
|
if (item.nodeType === 'FunctionDefinition') { |
||||||
|
return item.kind === 'function' ? original as string : item.kind; |
||||||
|
} else { |
||||||
|
return original as string; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function fullName ({ item, contract }: DocItemContext): string { |
||||||
|
if (contract) { |
||||||
|
return `${contract.name}.${item.name}`; |
||||||
|
} else { |
||||||
|
return `${item.name}`; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function signature ({ item }: DocItemContext): string | undefined { |
||||||
|
switch (item.nodeType) { |
||||||
|
case 'ContractDefinition': |
||||||
|
return undefined; |
||||||
|
|
||||||
|
case 'FunctionDefinition': { |
||||||
|
const { kind, name } = item; |
||||||
|
const params = item.parameters.parameters; |
||||||
|
const returns = item.returnParameters.parameters; |
||||||
|
const head = (kind === 'function' || kind === 'freeFunction') ? [kind, name].join(' ') : kind; |
||||||
|
const res = [ |
||||||
|
`${head}(${params.map(formatVariable).join(', ')})`, |
||||||
|
item.visibility, |
||||||
|
]; |
||||||
|
if (item.stateMutability !== 'nonpayable') { |
||||||
|
res.push(item.stateMutability); |
||||||
|
} |
||||||
|
if (item.virtual) { |
||||||
|
res.push('virtual'); |
||||||
|
} |
||||||
|
if (returns.length > 0) { |
||||||
|
res.push(`returns (${returns.map(formatVariable).join(', ')})`); |
||||||
|
} |
||||||
|
return res.join(' '); |
||||||
|
} |
||||||
|
|
||||||
|
case 'EventDefinition': { |
||||||
|
const params = item.parameters.parameters; |
||||||
|
return `event ${item.name}(${params.map(formatVariable).join(', ')})`; |
||||||
|
} |
||||||
|
|
||||||
|
case 'ErrorDefinition': { |
||||||
|
const params = item.parameters.parameters; |
||||||
|
return `error ${item.name}(${params.map(formatVariable).join(', ')})`; |
||||||
|
} |
||||||
|
|
||||||
|
case 'ModifierDefinition': { |
||||||
|
const params = item.parameters.parameters; |
||||||
|
return `modifier ${item.name}(${params.map(formatVariable).join(', ')})`; |
||||||
|
} |
||||||
|
|
||||||
|
case 'VariableDeclaration': |
||||||
|
return formatVariable(item); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
interface Param extends VariableDeclaration { |
||||||
|
type: string; |
||||||
|
natspec?: string; |
||||||
|
}; |
||||||
|
|
||||||
|
function getParams (params: ParameterList, natspec: NatSpec['params'] | NatSpec['returns']): Param[] { |
||||||
|
return params.parameters.map((p, i) => ({ |
||||||
|
...p, |
||||||
|
type: p.typeDescriptions.typeString!, |
||||||
|
natspec: natspec?.find((q, j) => q.name === undefined ? i === j : p.name === q.name)?.description, |
||||||
|
})); |
||||||
|
} |
||||||
|
|
||||||
|
export function params ({ item }: DocItemContext): Param[] | undefined { |
||||||
|
if ('parameters' in item) { |
||||||
|
return getParams(item.parameters, natspec(item[DOC_ITEM_CONTEXT]).params); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function returns ({ item }: DocItemContext): Param[] | undefined { |
||||||
|
if ('returnParameters' in item) { |
||||||
|
return getParams(item.returnParameters, natspec(item[DOC_ITEM_CONTEXT]).returns); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function items ({ item }: DocItemContext): DocItem[] | undefined { |
||||||
|
return (item.nodeType === 'ContractDefinition') |
||||||
|
? item.nodes.filter(isNodeType(docItemTypes)).filter(n => !('visibility' in n) || n.visibility !== 'private') |
||||||
|
: undefined; |
||||||
|
} |
||||||
|
|
||||||
|
export function functions ({ item }: DocItemContext): FunctionDefinition[] | undefined { |
||||||
|
return [...findAll('FunctionDefinition', item)].filter(f => f.visibility !== 'private'); |
||||||
|
} |
||||||
|
|
||||||
|
export function events ({ item }: DocItemContext): EventDefinition[] | undefined { |
||||||
|
return [...findAll('EventDefinition', item)]; |
||||||
|
} |
||||||
|
|
||||||
|
export function modifiers ({ item }: DocItemContext): ModifierDefinition[] | undefined { |
||||||
|
return [...findAll('ModifierDefinition', item)]; |
||||||
|
} |
||||||
|
|
||||||
|
export function errors ({ item }: DocItemContext): ErrorDefinition[] | undefined { |
||||||
|
return [...findAll('ErrorDefinition', item)]; |
||||||
|
} |
||||||
|
|
||||||
|
export function variables ({ item }: DocItemContext): VariableDeclaration[] | undefined { |
||||||
|
return (item.nodeType === 'ContractDefinition') |
||||||
|
? item.nodes.filter(isNodeType('VariableDeclaration')).filter(v => v.stateVariable && v.visibility !== 'private') |
||||||
|
: undefined; |
||||||
|
} |
||||||
|
|
||||||
|
export function types ({ item }: DocItemContext): TypeDefinition[] | undefined { |
||||||
|
return [...findAll(['StructDefinition', 'EnumDefinition', 'UserDefinedValueTypeDefinition'], item)]; |
||||||
|
} |
||||||
|
export function anchor({ item, contract }: DocItemContext) { |
||||||
|
let res = ''; |
||||||
|
if (contract) { |
||||||
|
res += contract.name + '-'; |
||||||
|
} |
||||||
|
res += item.name; |
||||||
|
if ('parameters' in item) { |
||||||
|
const signature = item.parameters.parameters.map(v => v.typeName.typeDescriptions.typeString).join(','); |
||||||
|
res += slug('(' + signature + ')'); |
||||||
|
} |
||||||
|
if (isNodeType('VariableDeclaration', item)) { |
||||||
|
res += '-' + slug(item.typeName.typeDescriptions.typeString); |
||||||
|
} |
||||||
|
return res; |
||||||
|
} |
||||||
|
|
||||||
|
export function inheritance ({ item, build }: DocItemContext) { |
||||||
|
if (!isNodeType('ContractDefinition', item)) { |
||||||
|
throw new Error('used inherited-items on non-contract'); |
||||||
|
} |
||||||
|
|
||||||
|
return item.linearizedBaseContracts |
||||||
|
.map(id => build.deref('ContractDefinition', id)) |
||||||
|
.filter((c, i) => c.name !== 'Context' || i === 0); |
||||||
|
} |
||||||
|
|
||||||
|
export function hasfunctions ({ item }: DocItemContext) { |
||||||
|
return (item as any).inheritance.some(c => c.functions.length > 0); |
||||||
|
} |
||||||
|
|
||||||
|
export function hasevents ({ item }: DocItemContext) { |
||||||
|
return (item as any).inheritance.some(c => c.events.length > 0); |
||||||
|
} |
||||||
|
|
||||||
|
export function inheritedfunctions ({ item }: DocItemContext) { |
||||||
|
const { inheritance } = (item as any) |
||||||
|
const baseFunctions = new Set(inheritance.flatMap(c => c.functions.flatMap(f => f.baseFunctions ?? []))); |
||||||
|
return inheritance.map((contract, i) => ({ |
||||||
|
contract, |
||||||
|
functions: contract.functions.filter(f => !baseFunctions.has(f.id) && (f.name !== 'constructor' || i === 0)), |
||||||
|
})) |
||||||
|
} |
@ -0,0 +1,84 @@ |
|||||||
|
import type { SourceUnit } from 'solidity-ast'; |
||||||
|
import type { DocItem } from './doc-item'; |
||||||
|
import type { PageAssigner, PageStructure } from './site'; |
||||||
|
|
||||||
|
export interface UserConfig { |
||||||
|
/** |
||||||
|
* The directory where rendered pages will be written. |
||||||
|
* Defaults to 'docs'. |
||||||
|
*/ |
||||||
|
outputDir?: string; |
||||||
|
|
||||||
|
/** |
||||||
|
* A directory of custom templates that should take precedence over the |
||||||
|
* theme's templates. |
||||||
|
*/ |
||||||
|
templates?: string; |
||||||
|
|
||||||
|
/** |
||||||
|
* The name of the built-in templates that will be used by default. |
||||||
|
* Defaults to 'markdown'. |
||||||
|
*/ |
||||||
|
theme?: string; |
||||||
|
|
||||||
|
/** |
||||||
|
* The way documentable items (contracts, functions, custom errors, etc.) |
||||||
|
* will be organized in pages. Built in options are: |
||||||
|
* - 'single': all items in one page |
||||||
|
* - 'items': one page per item |
||||||
|
* - 'files': one page per input Solidity file |
||||||
|
* More customization is possible by defining a function that returns a page |
||||||
|
* path given the AST node for the item and the source unit where it is |
||||||
|
* defined. |
||||||
|
* Defaults to 'single'. |
||||||
|
*/ |
||||||
|
pages?: 'single' | 'items' | 'files' | PageAssigner; |
||||||
|
|
||||||
|
/** |
||||||
|
* An array of sources subdirectories that should be excluded from |
||||||
|
* documentation, relative to the contract sources directory. |
||||||
|
*/ |
||||||
|
exclude?: string[]; |
||||||
|
|
||||||
|
/** |
||||||
|
* Clean up the output by collapsing 3 or more contiguous newlines into only 2. |
||||||
|
* Enabled by default. |
||||||
|
*/ |
||||||
|
collapseNewlines?: boolean; |
||||||
|
|
||||||
|
/** |
||||||
|
* The extension for generated pages. |
||||||
|
* Defaults to '.md'. |
||||||
|
*/ |
||||||
|
pageExtension?: string; |
||||||
|
} |
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Other config parameters that will be provided by the environment (e.g. Hardhat)
|
||||||
|
// rather than by the user manually, unless using the library directly.
|
||||||
|
export interface Config extends UserConfig { |
||||||
|
/** |
||||||
|
* The root directory relative to which 'outputDir', 'sourcesDir', and |
||||||
|
* 'templates' are specified. Defaults to the working directory. |
||||||
|
*/ |
||||||
|
root?: string; |
||||||
|
|
||||||
|
/** |
||||||
|
* The Solidity sources directory. |
||||||
|
*/ |
||||||
|
sourcesDir?: string; |
||||||
|
} |
||||||
|
|
||||||
|
export type FullConfig = Required<Config>; |
||||||
|
|
||||||
|
export const defaults: Omit<FullConfig, 'templates'> = { |
||||||
|
root: process.cwd(), |
||||||
|
sourcesDir: 'contracts', |
||||||
|
outputDir: 'docs', |
||||||
|
pages: 'single', |
||||||
|
exclude: [], |
||||||
|
theme: 'markdown', |
||||||
|
collapseNewlines: true, |
||||||
|
pageExtension: '.md', |
||||||
|
}; |
@ -0,0 +1,27 @@ |
|||||||
|
import { ContractDefinition, ImportDirective, PragmaDirective, SourceUnit, UsingForDirective } from "solidity-ast"; |
||||||
|
import { Node, NodeType, NodeTypeMap } from "solidity-ast/node"; |
||||||
|
import { AssertEqual } from "./utils/assert-equal-types"; |
||||||
|
|
||||||
|
export type DocItem = Exclude< |
||||||
|
SourceUnit['nodes'][number] | ContractDefinition['nodes'][number], |
||||||
|
ImportDirective | PragmaDirective | UsingForDirective |
||||||
|
>; |
||||||
|
|
||||||
|
export const docItemTypes = [ |
||||||
|
'ContractDefinition', |
||||||
|
'EnumDefinition', |
||||||
|
'ErrorDefinition', |
||||||
|
'EventDefinition', |
||||||
|
'FunctionDefinition', |
||||||
|
'ModifierDefinition', |
||||||
|
'StructDefinition', |
||||||
|
'UserDefinedValueTypeDefinition', |
||||||
|
'VariableDeclaration', |
||||||
|
] as const; |
||||||
|
|
||||||
|
// Make sure at compile time that docItemTypes contains exactly the node types of DocItem.
|
||||||
|
const _: AssertEqual<typeof docItemTypes[number], DocItem['nodeType']> = true; |
||||||
|
|
||||||
|
export function isDocItem(node: Node): node is DocItem { |
||||||
|
return (docItemTypes as readonly string[]).includes(node.nodeType); |
||||||
|
} |
@ -0,0 +1,87 @@ |
|||||||
|
import Handlebars, { RuntimeOptions } from 'handlebars'; |
||||||
|
import { Site, Page, DocItemWithContext, DOC_ITEM_CONTEXT } from './site'; |
||||||
|
import { Templates } from './templates'; |
||||||
|
import { itemType } from './utils/item-type'; |
||||||
|
import fs from 'fs'; |
||||||
|
|
||||||
|
export interface RenderedPage { |
||||||
|
id: string; |
||||||
|
contents: string; |
||||||
|
} |
||||||
|
|
||||||
|
interface TemplateOptions { |
||||||
|
data: { |
||||||
|
site: Site; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export function render(site: Site, templates: Templates, collapseNewlines?: boolean): RenderedPage[] { |
||||||
|
const renderPage = buildRenderer(templates); |
||||||
|
const renderedPages: RenderedPage[] = []; |
||||||
|
for (const page of site.pages) { |
||||||
|
let contents = renderPage(page, { data: { site } }); |
||||||
|
if (collapseNewlines) { |
||||||
|
contents = contents.replace(/\n{3,}/g, '\n\n'); |
||||||
|
} |
||||||
|
renderedPages.push({ |
||||||
|
id: page.id, |
||||||
|
contents, |
||||||
|
}); |
||||||
|
} |
||||||
|
return renderedPages; |
||||||
|
} |
||||||
|
|
||||||
|
export const itemPartialName = (item: DocItemWithContext) => itemType(item).replace(/ /g, '-').toLowerCase(); |
||||||
|
|
||||||
|
function itemPartial(item: DocItemWithContext, options?: RuntimeOptions) { |
||||||
|
if (!item.__item_context) { |
||||||
|
throw new Error(`Partial 'item' used in unsupported context (not a doc item)`); |
||||||
|
} |
||||||
|
const partial = options?.partials?.[itemPartialName(item)]; |
||||||
|
if (!partial) { |
||||||
|
throw new Error(`Missing partial '${itemPartialName(item)}'`); |
||||||
|
} |
||||||
|
return partial(item, options); |
||||||
|
} |
||||||
|
|
||||||
|
function readmeHelper(H: typeof Handlebars, path: string, opts: RuntimeOptions) { |
||||||
|
const items: DocItemWithContext[] = opts.data.root.items; |
||||||
|
const renderedItems = Object.fromEntries( |
||||||
|
items.map(item => [ |
||||||
|
item.name, |
||||||
|
new H.SafeString( |
||||||
|
H.compile('{{>item}}')(item, opts), |
||||||
|
), |
||||||
|
]), |
||||||
|
); |
||||||
|
return new H.SafeString( |
||||||
|
H.compile(fs.readFileSync(path, 'utf8'))(renderedItems, opts), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function buildRenderer(templates: Templates): (page: Page, options: TemplateOptions) => string { |
||||||
|
const pageTemplate = templates.partials?.page; |
||||||
|
if (pageTemplate === undefined) { |
||||||
|
throw new Error(`Missing 'page' template`); |
||||||
|
} |
||||||
|
|
||||||
|
const H = Handlebars.create(); |
||||||
|
|
||||||
|
for (const [name, getBody] of Object.entries(templates.partials ?? {})) { |
||||||
|
let partial: HandlebarsTemplateDelegate | undefined; |
||||||
|
H.registerPartial(name, (...args) => { |
||||||
|
partial ??= H.compile(getBody()); |
||||||
|
return partial(...args); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
H.registerHelper('readme', (path: string, opts: RuntimeOptions) => readmeHelper(H, path, opts)); |
||||||
|
|
||||||
|
for (const [name, fn] of Object.entries(templates.helpers ?? {})) { |
||||||
|
H.registerHelper(name, fn); |
||||||
|
} |
||||||
|
|
||||||
|
H.registerPartial('item', itemPartial); |
||||||
|
|
||||||
|
return H.compile('{{>page}}'); |
||||||
|
} |
@ -0,0 +1,138 @@ |
|||||||
|
import path from 'path'; |
||||||
|
import { ContractDefinition, SourceUnit } from 'solidity-ast'; |
||||||
|
import { SolcOutput, SolcInput } from 'solidity-ast/solc'; |
||||||
|
import { astDereferencer, ASTDereferencer, findAll, isNodeType, srcDecoder, SrcDecoder } from 'solidity-ast/utils'; |
||||||
|
import { FullConfig } from './config'; |
||||||
|
import { DocItem, docItemTypes, isDocItem } from './doc-item'; |
||||||
|
import { Properties } from './templates'; |
||||||
|
import { clone } from './utils/clone'; |
||||||
|
import { isChild } from './utils/is-child'; |
||||||
|
import { mapValues } from './utils/map-values'; |
||||||
|
import { defineGetterMemoized } from './utils/memoized-getter'; |
||||||
|
|
||||||
|
export interface Build { |
||||||
|
input: SolcInput; |
||||||
|
output: SolcOutput; |
||||||
|
} |
||||||
|
|
||||||
|
export interface BuildContext extends Build { |
||||||
|
deref: ASTDereferencer; |
||||||
|
decodeSrc: SrcDecoder; |
||||||
|
} |
||||||
|
|
||||||
|
export type SiteConfig = Pick<FullConfig, 'pages' | 'exclude' | 'sourcesDir' | 'pageExtension'>; |
||||||
|
export type PageStructure = SiteConfig['pages']; |
||||||
|
export type PageAssigner = ((item: DocItem, file: SourceUnit, config: SiteConfig) => string | undefined); |
||||||
|
|
||||||
|
export const pageAssigner: Record<PageStructure & string, PageAssigner> = { |
||||||
|
single: (_1, _2, { pageExtension: ext }) => 'index' + ext, |
||||||
|
items: (item, _, { pageExtension: ext }) => item.name + ext, |
||||||
|
files: (_, file, { pageExtension: ext, sourcesDir }) => |
||||||
|
path.relative(sourcesDir, file.absolutePath).replace('.sol', ext), |
||||||
|
}; |
||||||
|
|
||||||
|
export interface Site { |
||||||
|
items: DocItemWithContext[]; |
||||||
|
pages: Page[]; |
||||||
|
} |
||||||
|
|
||||||
|
export interface Page { |
||||||
|
id: string; |
||||||
|
items: DocItemWithContext[]; |
||||||
|
} |
||||||
|
|
||||||
|
export const DOC_ITEM_CONTEXT = '__item_context' as const; |
||||||
|
export type DocItemWithContext = DocItem & { [DOC_ITEM_CONTEXT]: DocItemContext }; |
||||||
|
|
||||||
|
export interface DocItemContext { |
||||||
|
page?: string; |
||||||
|
item: DocItemWithContext; |
||||||
|
contract?: ContractDefinition; |
||||||
|
file: SourceUnit; |
||||||
|
build: BuildContext; |
||||||
|
} |
||||||
|
|
||||||
|
export function buildSite (builds: Build[], siteConfig: SiteConfig, properties: Properties = {}): Site { |
||||||
|
const assign = typeof siteConfig.pages === 'string' ? pageAssigner[siteConfig.pages] : siteConfig.pages; |
||||||
|
|
||||||
|
const seen = new Set<string>(); |
||||||
|
const items: DocItemWithContext[] = []; |
||||||
|
const pages: Record<string, DocItemWithContext[]> = {}; |
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
for (let { input, output } of builds) { |
||||||
|
// Clone because we will mutate in order to add item context.
|
||||||
|
output = { ...output, sources: clone(output.sources) }; |
||||||
|
|
||||||
|
const deref = astDereferencer(output); |
||||||
|
const decodeSrc = srcDecoder(input, output); |
||||||
|
const build = { input, output, deref, decodeSrc }; |
||||||
|
|
||||||
|
for (const { ast: file } of Object.values(output.sources)) { |
||||||
|
const isNewFile = !seen.has(file.absolutePath); |
||||||
|
seen.add(file.absolutePath); |
||||||
|
|
||||||
|
for (const topLevelItem of file.nodes) { |
||||||
|
if (!isDocItem(topLevelItem)) continue; |
||||||
|
|
||||||
|
const page = assignIfIncludedSource(assign, topLevelItem, file, siteConfig); |
||||||
|
|
||||||
|
const withContext = defineContext(topLevelItem, build, file, page); |
||||||
|
defineProperties(withContext, properties); |
||||||
|
|
||||||
|
if (isNewFile && page !== undefined) { |
||||||
|
(pages[page] ??= []).push(withContext); |
||||||
|
items.push(withContext); |
||||||
|
} |
||||||
|
|
||||||
|
if (!isNodeType('ContractDefinition', topLevelItem)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
for (const item of topLevelItem.nodes) { |
||||||
|
if (!isDocItem(item)) continue; |
||||||
|
if (isNewFile && page !== undefined) items.push(item as DocItemWithContext); |
||||||
|
const contract = topLevelItem.nodeType === 'ContractDefinition' ? topLevelItem : undefined; |
||||||
|
const withContext = defineContext(item, build, file, page, contract); |
||||||
|
defineProperties(withContext, properties); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
items, |
||||||
|
pages: Object.entries(pages).map(([id, pageItems]) => ({ id, items: pageItems })), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function defineContext (item: DocItem, build: BuildContext, file: SourceUnit, page?: string, contract?: ContractDefinition): DocItemWithContext { |
||||||
|
return Object.assign(item, { |
||||||
|
[DOC_ITEM_CONTEXT]: { build, file, contract, page, item: item as DocItemWithContext }, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function defineProperties (item: DocItemWithContext, properties: Properties) { |
||||||
|
for (const [prop, fn] of Object.entries(properties)) { |
||||||
|
const original: unknown = (item as any)[prop]; |
||||||
|
defineGetterMemoized(item as any, prop, () => fn(item.__item_context, original)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function assignIfIncludedSource ( |
||||||
|
assign: PageAssigner, |
||||||
|
item: DocItem, |
||||||
|
file: SourceUnit, |
||||||
|
config: SiteConfig, |
||||||
|
) { |
||||||
|
return isFileIncluded(file.absolutePath, config) |
||||||
|
? assign(item, file, config) |
||||||
|
: undefined; |
||||||
|
} |
||||||
|
|
||||||
|
function isFileIncluded (file: string, config: SiteConfig) { |
||||||
|
return ( |
||||||
|
isChild(file, config.sourcesDir) && |
||||||
|
config.exclude.every(e => !isChild(file, path.join(config.sourcesDir, e))) |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,110 @@ |
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */ |
||||||
|
import { mapKeys } from './utils/map-keys'; |
||||||
|
import { DocItemContext } from './site'; |
||||||
|
|
||||||
|
import * as defaultProperties from './common/properties'; |
||||||
|
import * as themeHelpers from './themes/markdown/helpers' |
||||||
|
|
||||||
|
const common = require('./themes/markdown/common.hbs'); |
||||||
|
const contract = require('./themes/markdown/contract.hbs'); |
||||||
|
const enum_ = require('./themes/markdown/enum.hbs'); |
||||||
|
const error = require('./themes/markdown/error.hbs'); |
||||||
|
const event = require('./themes/markdown/event.hbs'); |
||||||
|
const function_ = require('./themes/markdown/function.hbs'); |
||||||
|
const modifier = require('./themes/markdown/modifier.hbs'); |
||||||
|
const page = require('./themes/markdown/page.hbs'); |
||||||
|
const struct = require('./themes/markdown/struct.hbs'); |
||||||
|
const variable = require('./themes/markdown/variable.hbs'); |
||||||
|
const userDefinedValueType = require('./themes/markdown/user-defined-value-type.hbs'); |
||||||
|
|
||||||
|
export type PropertyGetter = (ctx: DocItemContext, original?: unknown) => unknown; |
||||||
|
export type Properties = Record<string, PropertyGetter>; |
||||||
|
|
||||||
|
export interface Templates { |
||||||
|
partials?: Record<string, () => string>; |
||||||
|
helpers?: Record<string, (...args: unknown[]) => string>; |
||||||
|
properties?: Record<string, PropertyGetter>; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Loads the templates that will be used for rendering a site based on a |
||||||
|
* default theme and user templates. |
||||||
|
* |
||||||
|
* The result contains all partials, helpers, and property getters defined in |
||||||
|
* the user templates and the default theme, where the user's take precedence |
||||||
|
* if there is a clash. Additionally, all theme partials and helpers are |
||||||
|
* included with the theme prefix, e.g. `markdown/contract` will be a partial. |
||||||
|
*/ |
||||||
|
export async function loadTemplates(defaultTheme: string, root: string, userTemplatesPath?: string): Promise<Templates> { |
||||||
|
const themes = await readThemes(); |
||||||
|
|
||||||
|
// Initialize templates with the default theme.
|
||||||
|
const templates: Required<Templates> = { |
||||||
|
partials: { ...themes[defaultTheme]?.partials }, |
||||||
|
helpers: { ...themes[defaultTheme]?.helpers }, |
||||||
|
properties: { ...defaultProperties }, |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
// Add partials and helpers from all themes, prefixed with the theme name.
|
||||||
|
for (const [themeName, theme] of Object.entries(themes)) { |
||||||
|
const addPrefix = (k: string) => `${themeName}/${k}`; |
||||||
|
Object.assign(templates.partials, mapKeys(theme.partials, addPrefix)); |
||||||
|
Object.assign(templates.helpers, mapKeys(theme.helpers, addPrefix)); |
||||||
|
} |
||||||
|
|
||||||
|
return templates; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Read templates and helpers from a directory. |
||||||
|
*/ |
||||||
|
export async function readTemplates(): Promise<Required<Templates>> { |
||||||
|
return { |
||||||
|
partials: await readPartials(), |
||||||
|
helpers: await readHelpers('helpers'), |
||||||
|
properties: await readHelpers('properties'), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
async function readPartials() { |
||||||
|
const partials: NonNullable<Templates['partials']> = {}; |
||||||
|
|
||||||
|
partials["common"] = () => common |
||||||
|
partials["contract"] = () => contract |
||||||
|
partials["enum"] = () => enum_ |
||||||
|
partials["error"] = () => error |
||||||
|
partials["event"] = () => event |
||||||
|
partials["function"] = () => function_ |
||||||
|
partials["modifier"] = () => modifier |
||||||
|
partials["page"] = () => page |
||||||
|
partials["struct"] = () => struct |
||||||
|
partials["variable"] = () => variable |
||||||
|
partials["user-defined-value-type"] = () => userDefinedValueType |
||||||
|
|
||||||
|
return partials; |
||||||
|
} |
||||||
|
|
||||||
|
async function readHelpers(name: string) { |
||||||
|
|
||||||
|
const helpers: Record<string, (...args: any[]) => any> = {}; |
||||||
|
|
||||||
|
for (const name in themeHelpers) { |
||||||
|
if (typeof themeHelpers[name] === 'function') { |
||||||
|
helpers[name] = themeHelpers[name]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return helpers; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads all built-in themes into an object. Partials will always be found in |
||||||
|
* src/themes, whereas helpers may instead be found in dist/themes if TypeScript |
||||||
|
* can't be imported directly. |
||||||
|
*/ |
||||||
|
async function readThemes(): Promise<Record<string, Required<Templates>>> { |
||||||
|
const themes: Record<string, Required<Templates>> = {} |
||||||
|
themes['markdown'] = await readTemplates() |
||||||
|
return themes |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
{{h}} {{name}} |
||||||
|
|
||||||
|
{{#if signature}} |
||||||
|
```solidity |
||||||
|
{{{signature}}} |
||||||
|
``` |
||||||
|
{{/if}} |
||||||
|
|
||||||
|
{{{natspec.notice}}} |
||||||
|
|
||||||
|
{{#if natspec.dev}} |
||||||
|
_{{{natspec.dev}}}_ |
||||||
|
{{/if}} |
||||||
|
|
||||||
|
{{#if natspec.params}} |
||||||
|
{{h 2}} Parameters |
||||||
|
|
||||||
|
| Name | Type | Description | |
||||||
|
| ---- | ---- | ----------- | |
||||||
|
{{#each params}} |
||||||
|
| {{name}} | {{type}} | {{{joinLines natspec}}} | |
||||||
|
{{/each}} |
||||||
|
{{/if}} |
||||||
|
|
||||||
|
{{#if natspec.returns}} |
||||||
|
{{h 2}} Return Values |
||||||
|
|
||||||
|
| Name | Type | Description | |
||||||
|
| ---- | ---- | ----------- | |
||||||
|
{{#each returns}} |
||||||
|
| {{#if name}}{{name}}{{else}}[{{@index}}]{{/if}} | {{type}} | {{{joinLines natspec}}} | |
||||||
|
{{/each}} |
||||||
|
{{/if}} |
||||||
|
|
@ -0,0 +1,46 @@ |
|||||||
|
{{>common}} |
||||||
|
|
||||||
|
{{h 2}} Contract |
||||||
|
{{name}} : {{__item_context.file.absolutePath}} |
||||||
|
|
||||||
|
{{{natspec.dev}}} |
||||||
|
|
||||||
|
{{#if modifiers}} |
||||||
|
{{s}} |
||||||
|
{{h 2}} Modifiers: |
||||||
|
{{#each modifiers}} |
||||||
|
{{#hsection}} |
||||||
|
{{>item}} |
||||||
|
{{/hsection}} |
||||||
|
{{/each}} |
||||||
|
{{/if}} |
||||||
|
|
||||||
|
{{#if hasfunctions}} |
||||||
|
{{s}} |
||||||
|
{{h 2}} Functions: |
||||||
|
{{#each inheritedfunctions}} |
||||||
|
{{#unless @first}} |
||||||
|
inherits {{contract.name}}: |
||||||
|
{{/unless}} |
||||||
|
{{#each functions}} |
||||||
|
{{#hsection}} |
||||||
|
{{>item}} |
||||||
|
{{/hsection}} |
||||||
|
{{/each}} |
||||||
|
{{/each}} |
||||||
|
{{/if}} |
||||||
|
|
||||||
|
{{#if hasevents}} |
||||||
|
{{s}} |
||||||
|
{{h 2}} Events: |
||||||
|
{{#each inheritance}} |
||||||
|
{{#unless @first}} |
||||||
|
inherits {{name}}: |
||||||
|
{{/unless}} |
||||||
|
{{#each events}} |
||||||
|
{{#hsection}} |
||||||
|
{{>item}} |
||||||
|
{{/hsection}} |
||||||
|
{{/each}} |
||||||
|
{{/each}} |
||||||
|
{{/if}} |
@ -0,0 +1,9 @@ |
|||||||
|
{{>common}} |
||||||
|
|
||||||
|
```solidity |
||||||
|
enum {{name}} { |
||||||
|
{{#each members}} |
||||||
|
{{name}}{{#unless @last}},{{/unless}} |
||||||
|
{{/each}} |
||||||
|
} |
||||||
|
``` |
@ -0,0 +1 @@ |
|||||||
|
{{>common}} |
@ -0,0 +1 @@ |
|||||||
|
{{>common}} |
@ -0,0 +1 @@ |
|||||||
|
{{>common}} |
@ -0,0 +1,62 @@ |
|||||||
|
import { HelperOptions, Utils } from 'handlebars'; |
||||||
|
|
||||||
|
export * from '../../common/helpers'; |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a Markdown heading marker. An optional number increases the heading level. |
||||||
|
* |
||||||
|
* Input Output |
||||||
|
* {{h}} {{name}} # Name |
||||||
|
* {{h 2}} {{name}} ## Name |
||||||
|
*/ |
||||||
|
export function h(opts: HelperOptions): string; |
||||||
|
export function h(hsublevel: number, opts: HelperOptions): string; |
||||||
|
export function h(hsublevel: number | HelperOptions, opts?: HelperOptions) { |
||||||
|
const { hlevel } = getHLevel(hsublevel, opts); |
||||||
|
return new Array(hlevel).fill('#').join(''); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Delineates a section where headings should be increased by 1 or a custom number. |
||||||
|
* |
||||||
|
* {{#hsection}} |
||||||
|
* {{>partial-with-headings}} |
||||||
|
* {{/hsection}} |
||||||
|
*/ |
||||||
|
export function hsection(opts: HelperOptions): string; |
||||||
|
export function hsection(hsublevel: number, opts: HelperOptions): string; |
||||||
|
export function hsection(this: unknown, hsublevel: number | HelperOptions, opts?: HelperOptions) { |
||||||
|
let hlevel; |
||||||
|
({ hlevel, opts } = getHLevel(hsublevel, opts)); |
||||||
|
opts.data = Utils.createFrame(opts.data); |
||||||
|
opts.data.hlevel = hlevel; |
||||||
|
return opts.fn(this as unknown, opts); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a Markdown heading marker. An optional number increases the heading level. |
||||||
|
* |
||||||
|
* Input Output |
||||||
|
* {{h}} {{name}} # Name |
||||||
|
* {{h 2}} {{name}} ## Name |
||||||
|
*/ |
||||||
|
export function s(opts: HelperOptions): string; |
||||||
|
export function s(hsublevel: number, opts: HelperOptions): string; |
||||||
|
export function s(hsublevel: number | HelperOptions, opts?: HelperOptions) { |
||||||
|
return ' --- ' |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Helper for dealing with the optional hsublevel argument. |
||||||
|
*/ |
||||||
|
function getHLevel(hsublevel: number | HelperOptions, opts?: HelperOptions) { |
||||||
|
if (typeof hsublevel === 'number') { |
||||||
|
opts = opts!; |
||||||
|
hsublevel = Math.max(1, hsublevel); |
||||||
|
} else { |
||||||
|
opts = hsublevel; |
||||||
|
hsublevel = 1; |
||||||
|
} |
||||||
|
const contextHLevel: number = opts.data?.hlevel ?? 0; |
||||||
|
return { opts, hlevel: contextHLevel + hsublevel }; |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
{{>common}} |
@ -0,0 +1,8 @@ |
|||||||
|
# Solidity API |
||||||
|
|
||||||
|
{{#each items}} |
||||||
|
{{#hsection}} |
||||||
|
{{>item}} |
||||||
|
{{/hsection}} |
||||||
|
|
||||||
|
{{/each}} |
@ -0,0 +1,9 @@ |
|||||||
|
{{>common}} |
||||||
|
|
||||||
|
```solidity |
||||||
|
struct {{name}} { |
||||||
|
{{#each members}} |
||||||
|
{{{typeName.typeDescriptions.typeString}}} {{name}}; |
||||||
|
{{/each}} |
||||||
|
} |
||||||
|
``` |
@ -0,0 +1 @@ |
|||||||
|
{{>common}} |
@ -0,0 +1 @@ |
|||||||
|
{{>common}} |
@ -0,0 +1,13 @@ |
|||||||
|
import { DocItemWithContext, DOC_ITEM_CONTEXT } from '../site'; |
||||||
|
|
||||||
|
export class ItemError extends Error { |
||||||
|
constructor(msg: string, item: DocItemWithContext) { |
||||||
|
const ctx = item[DOC_ITEM_CONTEXT]; |
||||||
|
const src = ctx && ctx.build.decodeSrc(item); |
||||||
|
if (src) { |
||||||
|
super(msg + ` (${src})`); |
||||||
|
} else { |
||||||
|
super(msg); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
export function arraysEqual<T>(a: T[], b: T[]): boolean; |
||||||
|
export function arraysEqual<T, U>(a: T[], b: T[], mapFn: (x: T) => U): boolean; |
||||||
|
export function arraysEqual<T>(a: T[], b: T[], mapFn = (x: T) => x): boolean { |
||||||
|
return a.length === b.length && a.every((x, i) => mapFn(x) === mapFn(b[i]!)); |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export type AssertEqual<T, U> = [T, U] extends [U, T] ? true : never; |
@ -0,0 +1,6 @@ |
|||||||
|
/** |
||||||
|
* Deep cloning good enough for simple objects like solc output. Types are not |
||||||
|
* sound because the function may lose information: non-enumerable properties, |
||||||
|
* symbols, undefined values, prototypes, etc. |
||||||
|
*/ |
||||||
|
export const clone = <T>(obj: T): T => JSON.parse(JSON.stringify(obj)); |
@ -0,0 +1,12 @@ |
|||||||
|
// The function below would not be correctly typed if the return type was T[]
|
||||||
|
// because T may itself be an array type and Array.isArray would not know the
|
||||||
|
// difference. Adding IfArray<T> makes sure the return type is always correct.
|
||||||
|
type IfArray<T> = T extends any[] ? T : never; |
||||||
|
|
||||||
|
export function ensureArray<T>(x: T | T[]): T[] | IfArray<T> { |
||||||
|
if (Array.isArray(x)) { |
||||||
|
return x; |
||||||
|
} else { |
||||||
|
return [x]; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
/** |
||||||
|
* Iterates over all contiguous matches of the regular expression over the |
||||||
|
* text. Stops as soon as the regular expression no longer matches at the |
||||||
|
* current position. |
||||||
|
*/ |
||||||
|
export function* execAll(re: RegExp, text: string) { |
||||||
|
re = new RegExp(re, re.flags + (re.sticky ? '' : 'y')); |
||||||
|
|
||||||
|
while (true) { |
||||||
|
const match = re.exec(text); |
||||||
|
|
||||||
|
// We break out of the loop if there is no match or if the empty string is
|
||||||
|
// matched because no progress will be made and it will loop indefinitely.
|
||||||
|
if (!match?.[0]) break; |
||||||
|
|
||||||
|
yield match; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
import path from 'path'; |
||||||
|
|
||||||
|
export function isChild(file: string, parent: string) { |
||||||
|
return path.normalize(file + path.sep).startsWith(path.normalize(parent + path.sep)); |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
import { DocItem } from '../doc-item'; |
||||||
|
|
||||||
|
export function itemType(item: DocItem): string { |
||||||
|
return item.nodeType |
||||||
|
.replace(/(Definition|Declaration)$/, '') |
||||||
|
.replace(/(\w)([A-Z])/g, '$1 $2'); |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
export function mapKeys<T>(obj: Record<string, T>, fn: (key: string) => string): Record<string, T> { |
||||||
|
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [fn(k), v])); |
||||||
|
} |
||||||
|
|
@ -0,0 +1,19 @@ |
|||||||
|
export function mapValues<T, U>(obj: Record<string, T>, fn: (value: T) => U): Record<string, U> { |
||||||
|
const res: Record<string, U> = {}; |
||||||
|
for (const [k, v] of Object.entries(obj)) { |
||||||
|
res[k] = fn(v); |
||||||
|
} |
||||||
|
return res; |
||||||
|
} |
||||||
|
|
||||||
|
export function filterValues<T, U extends T>(obj: Record<string, T>, fn: (value: T) => value is U): Record<string, U>; |
||||||
|
export function filterValues<T>(obj: Record<string, T>, fn: (value: T) => boolean): Record<string, T>; |
||||||
|
export function filterValues<T>(obj: Record<string, T>, fn: (value: T) => boolean): Record<string, T> { |
||||||
|
const res: Record<string, T> = {}; |
||||||
|
for (const [k, v] of Object.entries(obj)) { |
||||||
|
if (fn(v)) { |
||||||
|
res[k] = v; |
||||||
|
} |
||||||
|
} |
||||||
|
return res; |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
export function defineGetterMemoized<K extends keyof any, T, O extends { [k in K]?: T }>(obj: O, key: K, getter: () => T) { |
||||||
|
let state: 'todo' | 'doing' | 'done' = 'todo'; |
||||||
|
let value: T; |
||||||
|
|
||||||
|
Object.defineProperty(obj, key, { |
||||||
|
enumerable: true, |
||||||
|
get() { |
||||||
|
switch (state) { |
||||||
|
case 'done':
|
||||||
|
return value; |
||||||
|
|
||||||
|
case 'doing': |
||||||
|
throw new Error("Detected recursion"); |
||||||
|
|
||||||
|
case 'todo': |
||||||
|
state = 'doing'; |
||||||
|
value = getter(); |
||||||
|
state = 'done'; |
||||||
|
return value; |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,145 @@ |
|||||||
|
import { FunctionDefinition } from 'solidity-ast'; |
||||||
|
import { findAll } from 'solidity-ast/utils'; |
||||||
|
import { DocItemWithContext, DOC_ITEM_CONTEXT } from '../site'; |
||||||
|
import { arraysEqual } from './arrays-equal'; |
||||||
|
import { execAll } from './execall'; |
||||||
|
import { itemType } from './item-type'; |
||||||
|
import { ItemError } from './ItemError'; |
||||||
|
import { readItemDocs } from './read-item-docs'; |
||||||
|
import { getContractsInScope } from './scope'; |
||||||
|
|
||||||
|
export interface NatSpec { |
||||||
|
title?: string; |
||||||
|
notice?: string; |
||||||
|
dev?: string; |
||||||
|
params?: { |
||||||
|
name: string; |
||||||
|
description: string; |
||||||
|
}[]; |
||||||
|
returns?: { |
||||||
|
name?: string; |
||||||
|
description: string; |
||||||
|
}[]; |
||||||
|
custom?: { |
||||||
|
[tag: string]: string; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export function parseNatspec(item: DocItemWithContext): NatSpec { |
||||||
|
if (!item[DOC_ITEM_CONTEXT]) throw new Error(`Not an item or item is missing context`); |
||||||
|
|
||||||
|
let res: NatSpec = {}; |
||||||
|
|
||||||
|
const docSource = readItemDocs(item); |
||||||
|
const docString = docSource !== undefined |
||||||
|
? cleanUpDocstringFromSource(docSource) |
||||||
|
: 'documentation' in item && item.documentation |
||||||
|
? typeof item.documentation === 'string' |
||||||
|
? item.documentation |
||||||
|
: cleanUpDocstringFromSolc(item.documentation.text) |
||||||
|
: ''; |
||||||
|
|
||||||
|
const tagMatches = execAll( |
||||||
|
/^(?:@(\w+|custom:[a-z][a-z-]*) )?((?:(?!^@(?:\w+|custom:[a-z][a-z-]*) )[^])*)/m, |
||||||
|
docString, |
||||||
|
); |
||||||
|
|
||||||
|
let inheritFrom: FunctionDefinition | undefined; |
||||||
|
|
||||||
|
for (const [, tag = 'notice', content] of tagMatches) { |
||||||
|
if (content === undefined) throw new ItemError('Unexpected error', item); |
||||||
|
|
||||||
|
if (tag === 'dev' || tag === 'notice') { |
||||||
|
res[tag] ??= ''; |
||||||
|
res[tag] += content; |
||||||
|
} |
||||||
|
|
||||||
|
if (tag === 'title') { |
||||||
|
res.title = content.trim(); |
||||||
|
} |
||||||
|
|
||||||
|
if (tag === 'param') { |
||||||
|
const paramMatches = content.match(/(\w+) ([^]*)/); |
||||||
|
if (paramMatches) { |
||||||
|
const [, name, description] = paramMatches as [string, string, string]; |
||||||
|
res.params ??= []; |
||||||
|
res.params.push({ name, description: description.trim() }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (tag === 'return') { |
||||||
|
if (!('returnParameters' in item)) { |
||||||
|
throw new ItemError(`Item does not contain return parameters`, item); |
||||||
|
} |
||||||
|
res.returns ??= []; |
||||||
|
const i = res.returns.length; |
||||||
|
const p = item.returnParameters.parameters[i]; |
||||||
|
if (p === undefined) { |
||||||
|
throw new ItemError('Got more @return tags than expected', item); |
||||||
|
} |
||||||
|
if (!p.name) { |
||||||
|
res.returns.push({ description: content.trim() }); |
||||||
|
} else { |
||||||
|
const paramMatches = content.match(/(\w+)( ([^]*))?/); |
||||||
|
if (!paramMatches || paramMatches[1] !== p.name) { |
||||||
|
throw new ItemError(`Expected @return tag to start with name '${p.name}'`, item); |
||||||
|
} |
||||||
|
const [, name, description] = paramMatches as [string, string, string?]; |
||||||
|
res.returns.push({ name, description: description?.trim() ?? '' }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (tag?.startsWith('custom:')) { |
||||||
|
const key = tag.replace(/^custom:/, ''); |
||||||
|
res.custom ??= {}; |
||||||
|
res.custom[key] ??= ''; |
||||||
|
res.custom[key] += content; |
||||||
|
} |
||||||
|
|
||||||
|
if (tag === 'inheritdoc') { |
||||||
|
if (!(item.nodeType === 'FunctionDefinition' || item.nodeType === 'VariableDeclaration')) { |
||||||
|
throw new ItemError(`Expected function or variable but saw ${itemType(item)}`, item); |
||||||
|
} |
||||||
|
const parentContractName = content.trim(); |
||||||
|
const parentContract = getContractsInScope(item)[parentContractName]; |
||||||
|
if (!parentContract) { |
||||||
|
throw new ItemError(`Parent contract '${parentContractName}' not found`, item); |
||||||
|
} |
||||||
|
inheritFrom = [...findAll('FunctionDefinition', parentContract)].find(f => item.baseFunctions?.includes(f.id)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (docString.length === 0) { |
||||||
|
if ('baseFunctions' in item && item.baseFunctions?.length === 1) { |
||||||
|
const baseFn = item[DOC_ITEM_CONTEXT].build.deref('FunctionDefinition', item.baseFunctions[0]!); |
||||||
|
const shouldInherit = item.nodeType === 'VariableDeclaration' || arraysEqual(item.parameters.parameters, baseFn.parameters.parameters, p => p.name); |
||||||
|
if (shouldInherit) { |
||||||
|
inheritFrom = baseFn; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (res.dev) res.dev = res.dev.trim(); |
||||||
|
if (res.notice) res.notice = res.notice.trim(); |
||||||
|
|
||||||
|
if (inheritFrom) { |
||||||
|
res = { ...parseNatspec(inheritFrom as DocItemWithContext), ...res }; |
||||||
|
} |
||||||
|
|
||||||
|
return res; |
||||||
|
} |
||||||
|
|
||||||
|
// Fix solc buggy parsing of doc comments.
|
||||||
|
// Reverse engineered from solc behavior.
|
||||||
|
function cleanUpDocstringFromSolc(text: string) { |
||||||
|
return text |
||||||
|
.replace(/\n\n?^[ \t]*(?:\*|\/\/\/)/mg, '\n\n') |
||||||
|
.replace(/^[ \t]?/mg, ''); |
||||||
|
} |
||||||
|
|
||||||
|
function cleanUpDocstringFromSource(text: string) { |
||||||
|
return text |
||||||
|
.replace(/^\/\*\*(.*)\*\/$/s, '$1') |
||||||
|
.trim() |
||||||
|
.replace(/^[ \t]*(\*|\/\/\/)[ \t]?/mg, ''); |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
|
||||||
|
export function normalizeContractPath(contractPath: string): string[]{ |
||||||
|
const paths = contractPath.split('/') |
||||||
|
const filename = paths[paths.length - 1] |
||||||
|
let folders = '' |
||||||
|
for (let i = 0; i < paths.length - 1; i++) { |
||||||
|
if(i !== paths.length -1) { |
||||||
|
folders += `${paths[i]}/` |
||||||
|
} |
||||||
|
} |
||||||
|
const resultingPath = `${folders}${filename}` |
||||||
|
return [folders,resultingPath, filename] |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
import { DocItemWithContext, DOC_ITEM_CONTEXT, Build } from '../site'; |
||||||
|
|
||||||
|
export function readItemDocs(item: DocItemWithContext): string | undefined { |
||||||
|
const { build } = item[DOC_ITEM_CONTEXT]; |
||||||
|
// Note that Solidity 0.5 has item.documentation: string even though the
|
||||||
|
// types do not reflect that. This is why we check typeof === object.
|
||||||
|
if ('documentation' in item && item.documentation && typeof item.documentation === 'object') { |
||||||
|
const { source, start, length } = decodeSrc(item.documentation.src, build); |
||||||
|
const content = build.input.sources[source]?.content; |
||||||
|
if (content !== undefined) { |
||||||
|
return Buffer.from(content, 'utf8').slice(start, start + length).toString('utf8'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function decodeSrc(src: string, build: Build): { source: string; start: number; length: number } { |
||||||
|
const [start, length, sourceId] = src.split(':').map(s => parseInt(s)); |
||||||
|
if (start === undefined || length === undefined || sourceId === undefined) { |
||||||
|
throw new Error(`Bad source string ${src}`); |
||||||
|
} |
||||||
|
const source = Object.keys(build.output.sources).find(s => build.output.sources[s]?.id === sourceId); |
||||||
|
if (source === undefined) { |
||||||
|
throw new Error(`No source with id ${sourceId}`); |
||||||
|
} |
||||||
|
return { source, start, length }; |
||||||
|
} |
@ -0,0 +1,63 @@ |
|||||||
|
import { ContractDefinition, SourceUnit } from "solidity-ast"; |
||||||
|
import { findAll, isNodeType } from "solidity-ast/utils"; |
||||||
|
import { DocItemWithContext } from "../site"; |
||||||
|
import { filterValues, mapValues } from './map-values'; |
||||||
|
import { mapKeys } from './map-keys'; |
||||||
|
|
||||||
|
type Definition = SourceUnit['nodes'][number] & { name: string }; |
||||||
|
type Scope = { [name in string]: () => { namespace: Scope } | { definition: Definition } }; |
||||||
|
|
||||||
|
export function getContractsInScope(item: DocItemWithContext) { |
||||||
|
const cache = new WeakMap<SourceUnit, Scope>(); |
||||||
|
|
||||||
|
return filterValues( |
||||||
|
flattenScope(run(item.__item_context.file)), |
||||||
|
isNodeType('ContractDefinition'), |
||||||
|
); |
||||||
|
|
||||||
|
function run(file: SourceUnit): Scope { |
||||||
|
if (cache.has(file)) { |
||||||
|
return cache.get(file)!; |
||||||
|
} |
||||||
|
|
||||||
|
const scope: Scope = {}; |
||||||
|
|
||||||
|
cache.set(file, scope); |
||||||
|
|
||||||
|
for (const c of file.nodes) { |
||||||
|
if ('name' in c) { |
||||||
|
scope[c.name] = () => ({ definition: c }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (const i of findAll('ImportDirective', file)) { |
||||||
|
const importedFile = item.__item_context.build.deref('SourceUnit', i.sourceUnit); |
||||||
|
const importedScope = run(importedFile); |
||||||
|
if (i.unitAlias) { |
||||||
|
scope[i.unitAlias] = () => ({ namespace: importedScope }); |
||||||
|
} else if (i.symbolAliases.length === 0) { |
||||||
|
Object.assign(scope, importedScope); |
||||||
|
} else { |
||||||
|
for (const a of i.symbolAliases) { |
||||||
|
// Delayed function call supports circular dependencies
|
||||||
|
scope[a.local ?? a.foreign.name] = importedScope[a.foreign.name] ?? (() => importedScope[a.foreign.name]!()); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return scope; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function flattenScope(scope: Scope): Record<string, Definition> { |
||||||
|
return Object.fromEntries( |
||||||
|
Object.entries(scope).flatMap(([k, fn]) => { |
||||||
|
const v = fn(); |
||||||
|
if ('definition' in v) { |
||||||
|
return [[k, v.definition] as const]; |
||||||
|
} else { |
||||||
|
return Object.entries(mapKeys(flattenScope(v.namespace), k2 => k + '.' + k2)); |
||||||
|
} |
||||||
|
}), |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
import { useState } from "react"; |
||||||
|
|
||||||
|
export function useLocalStorage(key: string, initialValue: any) { |
||||||
|
// State to store our value
|
||||||
|
// Pass initial state function to useState so logic is only executed once
|
||||||
|
const [storedValue, setStoredValue] = useState(() => { |
||||||
|
try { |
||||||
|
// Get from local storage by key
|
||||||
|
const item = window.localStorage.getItem(key); |
||||||
|
// Parse stored json or if none return initialValue
|
||||||
|
return item ? JSON.parse(item) : initialValue; |
||||||
|
} catch (error) { |
||||||
|
// If error also return initialValue
|
||||||
|
console.log(error); |
||||||
|
return initialValue; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Return a wrapped version of useState's setter function that ...
|
||||||
|
// ... persists the new value to localStorage.
|
||||||
|
const setValue = (value: any) => { |
||||||
|
try { |
||||||
|
// Allow value to be a function so we have same API as useState
|
||||||
|
const valueToStore = |
||||||
|
value instanceof Function ? value(storedValue) : value; |
||||||
|
// Save state
|
||||||
|
setStoredValue(valueToStore); |
||||||
|
// Save to local storage
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(valueToStore)); |
||||||
|
} catch (error) { |
||||||
|
// A more advanced implementation would handle the error case
|
||||||
|
console.log(error); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return [storedValue, setValue]; |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
import React from "react"; |
||||||
|
|
||||||
|
export const ErrorView: React.FC = () => { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
style={{ |
||||||
|
width: "100%", |
||||||
|
display: "flex", |
||||||
|
flexDirection: "column", |
||||||
|
alignItems: "center", |
||||||
|
}} |
||||||
|
> |
||||||
|
<img |
||||||
|
style={{ paddingBottom: "2em" }} |
||||||
|
width="250" |
||||||
|
src="https://res.cloudinary.com/key-solutions/image/upload/v1580400635/solid/error-png.png" |
||||||
|
alt="Error page" |
||||||
|
/> |
||||||
|
<h5>Sorry, something unexpected happened. </h5> |
||||||
|
<h5> |
||||||
|
Please raise an issue:{" "} |
||||||
|
<a |
||||||
|
style={{ color: "red" }} |
||||||
|
href="https://github.com/Machinalabs/remix-ethdoc-plugin/issues" |
||||||
|
> |
||||||
|
Here |
||||||
|
</a> |
||||||
|
</h5> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1 @@ |
|||||||
|
|
After Width: | Height: | Size: 3.1 KiB |
@ -0,0 +1,13 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8" /> |
||||||
|
<title>Documentation generator</title> |
||||||
|
<base href="/" /> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" /> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="root"></div> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,11 @@ |
|||||||
|
import React from "react"; |
||||||
|
import ReactDOM from "react-dom"; |
||||||
|
import App from "./app/App"; |
||||||
|
// import { Routes } from "./routes";
|
||||||
|
|
||||||
|
ReactDOM.render( |
||||||
|
<React.StrictMode> |
||||||
|
<App /> |
||||||
|
</React.StrictMode>, |
||||||
|
document.getElementById("root") |
||||||
|
); |
@ -0,0 +1,17 @@ |
|||||||
|
{ |
||||||
|
"name": "doc-gen", |
||||||
|
"displayName": "Docgen - Documentation Generator", |
||||||
|
"description": "Generate Solidity documentation (as md)", |
||||||
|
"version": "0.1.0", |
||||||
|
"events": [], |
||||||
|
"methods": ["generateDocs", "openDocs"], |
||||||
|
"kind": "none", |
||||||
|
"icon": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSIxMDI0IiB3aWR0aD0iMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOTUwLjE1NCAxOTJINzMuODQ2QzMzLjEyNyAxOTIgMCAyMjUuMTI2OTk5OTk5OTk5OTUgMCAyNjUuODQ2djQ5Mi4zMDhDMCA3OTguODc1IDMzLjEyNyA4MzIgNzMuODQ2IDgzMmg4NzYuMzA4YzQwLjcyMSAwIDczLjg0Ni0zMy4xMjUgNzMuODQ2LTczLjg0NlYyNjUuODQ2QzEwMjQgMjI1LjEyNjk5OTk5OTk5OTk1IDk5MC44NzUgMTkyIDk1MC4xNTQgMTkyek01NzYgNzAzLjg3NUw0NDggNzA0VjUxMmwtOTYgMTIzLjA3N0wyNTYgNTEydjE5MkgxMjhWMzIwaDEyOGw5NiAxMjggOTYtMTI4IDEyOC0wLjEyNVY3MDMuODc1ek03NjcuMDkxIDczNS44NzVMNjA4IDUxMmg5NlYzMjBoMTI4djE5Mmg5Nkw3NjcuMDkxIDczNS44NzV6Ii8+PC9zdmc+", |
||||||
|
"location": "sidePanel", |
||||||
|
"documentation": "", |
||||||
|
"repo": "https://github.com/ethereum/remix-project/", |
||||||
|
"maintainedBy": "", |
||||||
|
"authorContact": "", |
||||||
|
"url": "", |
||||||
|
"targets":["remix"] |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
export type Documentation = string |
||||||
|
|
||||||
|
export interface EthDocumentation { |
||||||
|
[contractName: string]: Documentation |
||||||
|
} |
||||||
|
|
||||||
|
export type ContractName = string |
||||||
|
|
||||||
|
export type FileName = string |
||||||
|
|
||||||
|
export type PublishedSite = string |
@ -0,0 +1,23 @@ |
|||||||
|
{ |
||||||
|
"extends": "./tsconfig.json", |
||||||
|
"compilerOptions": { |
||||||
|
"outDir": "../../dist/out-tsc", |
||||||
|
"types": ["node"] |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts", |
||||||
|
"../../node_modules/@nrwl/react/typings/image.d.ts" |
||||||
|
], |
||||||
|
"exclude": [ |
||||||
|
"jest.config.ts", |
||||||
|
"**/*.spec.ts", |
||||||
|
"**/*.test.ts", |
||||||
|
"**/*.spec.tsx", |
||||||
|
"**/*.test.tsx", |
||||||
|
"**/*.spec.js", |
||||||
|
"**/*.test.js", |
||||||
|
"**/*.spec.jsx", |
||||||
|
"**/*.test.jsx" |
||||||
|
], |
||||||
|
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
{ |
||||||
|
"extends": "../../tsconfig.base.json", |
||||||
|
"compilerOptions": { |
||||||
|
"jsx": "react-jsx", |
||||||
|
"allowJs": true, |
||||||
|
"esModuleInterop": true, |
||||||
|
"allowSyntheticDefaultImports": true |
||||||
|
}, |
||||||
|
"files": [], |
||||||
|
"include": [], |
||||||
|
"references": [ |
||||||
|
{ |
||||||
|
"path": "./tsconfig.app.json" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,73 @@ |
|||||||
|
const { composePlugins, withNx } = require('@nrwl/webpack') |
||||||
|
const { withReact } = require('@nrwl/react') |
||||||
|
const webpack = require('webpack') |
||||||
|
const TerserPlugin = require("terser-webpack-plugin") |
||||||
|
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin") |
||||||
|
|
||||||
|
// Nx plugins for webpack.
|
||||||
|
module.exports = composePlugins(withNx(), withReact(), (config) => { |
||||||
|
// Update the webpack config as needed here.
|
||||||
|
// e.g. `config.plugins.push(new MyPlugin())`
|
||||||
|
|
||||||
|
// add fallback for node modules
|
||||||
|
config.resolve.fallback = { |
||||||
|
...config.resolve.fallback, |
||||||
|
"path": require.resolve("path-browserify"), |
||||||
|
"fs": false, |
||||||
|
} |
||||||
|
|
||||||
|
// add externals
|
||||||
|
config.externals = { |
||||||
|
...config.externals, |
||||||
|
solc: 'solc', |
||||||
|
} |
||||||
|
|
||||||
|
config.module.rules.push({ |
||||||
|
test: /\.hbs$/, |
||||||
|
type: 'asset/source' |
||||||
|
}) |
||||||
|
|
||||||
|
|
||||||
|
// add public path
|
||||||
|
config.output.publicPath = '/' |
||||||
|
|
||||||
|
// add copy & provide plugin
|
||||||
|
config.plugins.push( |
||||||
|
new webpack.ProvidePlugin({ |
||||||
|
Buffer: ['buffer', 'Buffer'], |
||||||
|
url: ['url', 'URL'], |
||||||
|
process: 'process/browser', |
||||||
|
}), |
||||||
|
new webpack.DefinePlugin({ |
||||||
|
|
||||||
|
}), |
||||||
|
) |
||||||
|
|
||||||
|
// souce-map loader
|
||||||
|
config.module.rules.push({ |
||||||
|
test: /\.js$/, |
||||||
|
use: ["source-map-loader"], |
||||||
|
enforce: "pre" |
||||||
|
}) |
||||||
|
|
||||||
|
config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings
|
||||||
|
|
||||||
|
// set minimizer
|
||||||
|
config.optimization.minimizer = [ |
||||||
|
new TerserPlugin({ |
||||||
|
parallel: true, |
||||||
|
terserOptions: { |
||||||
|
ecma: 2015, |
||||||
|
compress: false, |
||||||
|
mangle: false, |
||||||
|
format: { |
||||||
|
comments: false, |
||||||
|
}, |
||||||
|
}, |
||||||
|
extractComments: false, |
||||||
|
}), |
||||||
|
new CssMinimizerPlugin(), |
||||||
|
]; |
||||||
|
|
||||||
|
return config; |
||||||
|
}) |
@ -0,0 +1,61 @@ |
|||||||
|
{ |
||||||
|
"name": "doc-viewer", |
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json", |
||||||
|
"sourceRoot": "apps/doc-viewer/src", |
||||||
|
"projectType": "application", |
||||||
|
"implicitDependencies": [ |
||||||
|
], |
||||||
|
"targets": { |
||||||
|
"build": { |
||||||
|
"executor": "@nrwl/webpack:webpack", |
||||||
|
"outputs": ["{options.outputPath}"], |
||||||
|
"defaultConfiguration": "development", |
||||||
|
"options": { |
||||||
|
"compiler": "babel", |
||||||
|
"outputPath": "dist/apps/doc-viewer", |
||||||
|
"index": "apps/doc-viewer/src/index.html", |
||||||
|
"baseHref": "./", |
||||||
|
"main": "apps/doc-viewer/src/main.tsx", |
||||||
|
"tsConfig": "apps/doc-viewer/tsconfig.app.json", |
||||||
|
"assets": [ |
||||||
|
"apps/doc-viewer/src/favicon.ico", |
||||||
|
"apps/doc-viewer/src/profile.json" |
||||||
|
], |
||||||
|
"styles": [], |
||||||
|
"scripts": [], |
||||||
|
"webpackConfig": "apps/doc-viewer/webpack.config.js" |
||||||
|
}, |
||||||
|
"configurations": { |
||||||
|
"development": { |
||||||
|
}, |
||||||
|
"production": { |
||||||
|
"fileReplacements": [ |
||||||
|
{ |
||||||
|
"replace": "apps/doc-viewer/src/environments/environment.ts", |
||||||
|
"with": "apps/doc-viewer/src/environments/environment.prod.ts" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
"serve": { |
||||||
|
"executor": "@nrwl/webpack:dev-server", |
||||||
|
"defaultConfiguration": "development", |
||||||
|
"options": { |
||||||
|
"buildTarget": "doc-viewer:build", |
||||||
|
"hmr": true, |
||||||
|
"baseHref": "/" |
||||||
|
}, |
||||||
|
"configurations": { |
||||||
|
"development": { |
||||||
|
"buildTarget": "doc-viewer:build:development", |
||||||
|
"port": 7003 |
||||||
|
}, |
||||||
|
"production": { |
||||||
|
"buildTarget": "doc-viewer:build:production" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
"tags": [] |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
import React, { useEffect, useState } from "react" |
||||||
|
import { DocViewer } from "./docviewer" |
||||||
|
import ReactMarkdown from 'react-markdown' |
||||||
|
import remarkGfm from 'remark-gfm' |
||||||
|
|
||||||
|
const client = new DocViewer() |
||||||
|
|
||||||
|
export default function App() { |
||||||
|
const [contents, setContents] = useState('') |
||||||
|
useEffect(() => { |
||||||
|
client.eventEmitter.on('contentsReady', (fileContents: string) => { |
||||||
|
setContents(fileContents) |
||||||
|
}) |
||||||
|
|
||||||
|
}, []) |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<div className="m-5 p-2"> |
||||||
|
<ReactMarkdown children={contents} remarkPlugins={[remarkGfm]}/> |
||||||
|
</div> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
import { PluginClient } from '@remixproject/plugin' |
||||||
|
import { createClient } from '@remixproject/plugin-webview' |
||||||
|
import EventEmitter from 'events' |
||||||
|
|
||||||
|
export class DocViewer extends PluginClient { |
||||||
|
mdFile: string |
||||||
|
eventEmitter: EventEmitter |
||||||
|
constructor() { |
||||||
|
super() |
||||||
|
this.eventEmitter = new EventEmitter() |
||||||
|
this.methods = ['viewDocs'] |
||||||
|
createClient(this) |
||||||
|
this.mdFile = '' |
||||||
|
this.onload() |
||||||
|
} |
||||||
|
|
||||||
|
async viewDocs(docs: string[]) { |
||||||
|
this.mdFile = docs[0] |
||||||
|
const contents = await this.call('fileManager', 'readFile', this.mdFile) |
||||||
|
this.eventEmitter.emit('contentsReady', contents) |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 3.1 KiB |
@ -0,0 +1,13 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8" /> |
||||||
|
<title>Doc Viewer</title> |
||||||
|
<base href="/" /> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" /> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="root"></div> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,10 @@ |
|||||||
|
import React from 'react' |
||||||
|
import ReactDOM from 'react-dom' |
||||||
|
import App from './app/App' |
||||||
|
|
||||||
|
ReactDOM.render( |
||||||
|
<React.StrictMode> |
||||||
|
<App /> |
||||||
|
</React.StrictMode>, |
||||||
|
document.getElementById("root") |
||||||
|
); |
@ -0,0 +1,16 @@ |
|||||||
|
{ |
||||||
|
"name": "doc-viewer", |
||||||
|
"displayName": "Docgen Viewer", |
||||||
|
"description": "Visualize Solidity documentation from Docgen Plugin", |
||||||
|
"version": "0.1.0", |
||||||
|
"events": [], |
||||||
|
"methods": ["viewDoc"], |
||||||
|
"kind": "none", |
||||||
|
"icon": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSIxMDI0IiB3aWR0aD0iMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOTUwLjE1NCAxOTJINzMuODQ2QzMzLjEyNyAxOTIgMCAyMjUuMTI2OTk5OTk5OTk5OTUgMCAyNjUuODQ2djQ5Mi4zMDhDMCA3OTguODc1IDMzLjEyNyA4MzIgNzMuODQ2IDgzMmg4NzYuMzA4YzQwLjcyMSAwIDczLjg0Ni0zMy4xMjUgNzMuODQ2LTczLjg0NlYyNjUuODQ2QzEwMjQgMjI1LjEyNjk5OTk5OTk5OTk1IDk5MC44NzUgMTkyIDk1MC4xNTQgMTkyek01NzYgNzAzLjg3NUw0NDggNzA0VjUxMmwtOTYgMTIzLjA3N0wyNTYgNTEydjE5MkgxMjhWMzIwaDEyOGw5NiAxMjggOTYtMTI4IDEyOC0wLjEyNVY3MDMuODc1ek03NjcuMDkxIDczNS44NzVMNjA4IDUxMmg5NlYzMjBoMTI4djE5Mmg5Nkw3NjcuMDkxIDczNS44NzV6Ii8+PC9zdmc+", |
||||||
|
"location": "mainPanel", |
||||||
|
"url": "", |
||||||
|
"documentation": "https://remix-plugins.readthedocs.io/en/latest/", |
||||||
|
"repo": "https://github.com/Machinalabs/remix-ethdoc-plugin/", |
||||||
|
"maintainedBy": "Remix", |
||||||
|
"authorContact": "remix@ethereum.org" |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
{ |
||||||
|
"extends": "./tsconfig.json", |
||||||
|
"compilerOptions": { |
||||||
|
"outDir": "../../dist/out-tsc", |
||||||
|
"types": ["node"] |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts", |
||||||
|
"../../node_modules/@nrwl/react/typings/image.d.ts" |
||||||
|
], |
||||||
|
"exclude": [ |
||||||
|
"jest.config.ts", |
||||||
|
"**/*.spec.ts", |
||||||
|
"**/*.test.ts", |
||||||
|
"**/*.spec.tsx", |
||||||
|
"**/*.test.tsx", |
||||||
|
"**/*.spec.js", |
||||||
|
"**/*.test.js", |
||||||
|
"**/*.spec.jsx", |
||||||
|
"**/*.test.jsx" |
||||||
|
], |
||||||
|
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
{ |
||||||
|
"extends": "../../tsconfig.base.json", |
||||||
|
"compilerOptions": { |
||||||
|
"jsx": "react-jsx", |
||||||
|
"allowJs": true, |
||||||
|
"esModuleInterop": true, |
||||||
|
"allowSyntheticDefaultImports": true |
||||||
|
}, |
||||||
|
"files": [], |
||||||
|
"include": [], |
||||||
|
"references": [ |
||||||
|
{ |
||||||
|
"path": "./tsconfig.app.json" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,60 @@ |
|||||||
|
const { composePlugins, withNx } = require('@nrwl/webpack') |
||||||
|
const { withReact } = require('@nrwl/react') |
||||||
|
const webpack = require('webpack') |
||||||
|
const TerserPlugin = require("terser-webpack-plugin") |
||||||
|
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin") |
||||||
|
|
||||||
|
// Nx plugins for webpack.
|
||||||
|
module.exports = composePlugins(withNx(), withReact(), (config) => { |
||||||
|
// Update the webpack config as needed here.
|
||||||
|
// e.g. `config.plugins.push(new MyPlugin())`
|
||||||
|
|
||||||
|
// add externals
|
||||||
|
config.externals = { |
||||||
|
...config.externals, |
||||||
|
solc: 'solc', |
||||||
|
} |
||||||
|
|
||||||
|
// add public path
|
||||||
|
config.output.publicPath = '/' |
||||||
|
|
||||||
|
// add copy & provide plugin
|
||||||
|
config.plugins.push( |
||||||
|
new webpack.ProvidePlugin({ |
||||||
|
Buffer: ['buffer', 'Buffer'], |
||||||
|
url: ['url', 'URL'], |
||||||
|
process: 'process/browser', |
||||||
|
}), |
||||||
|
new webpack.DefinePlugin({ |
||||||
|
|
||||||
|
}), |
||||||
|
) |
||||||
|
|
||||||
|
// souce-map loader
|
||||||
|
config.module.rules.push({ |
||||||
|
test: /\.js$/, |
||||||
|
use: ["source-map-loader"], |
||||||
|
enforce: "pre" |
||||||
|
}) |
||||||
|
|
||||||
|
config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings
|
||||||
|
|
||||||
|
// set minimizer
|
||||||
|
config.optimization.minimizer = [ |
||||||
|
new TerserPlugin({ |
||||||
|
parallel: true, |
||||||
|
terserOptions: { |
||||||
|
ecma: 2015, |
||||||
|
compress: false, |
||||||
|
mangle: false, |
||||||
|
format: { |
||||||
|
comments: false, |
||||||
|
}, |
||||||
|
}, |
||||||
|
extractComments: false, |
||||||
|
}), |
||||||
|
new CssMinimizerPlugin(), |
||||||
|
]; |
||||||
|
|
||||||
|
return config; |
||||||
|
}) |
@ -1,37 +1,47 @@ |
|||||||
import React from "react" |
import React from "react" |
||||||
|
import { CustomTooltip } from '@remix-ui/helper' |
||||||
|
|
||||||
interface Props { |
interface Props { |
||||||
text: string |
text: string |
||||||
isSubmitting?: boolean |
isSubmitting?: boolean |
||||||
dataId?: string |
dataId?: string |
||||||
|
disable?: boolean |
||||||
} |
} |
||||||
|
|
||||||
export const SubmitButton: React.FC<Props> = ({ |
export const SubmitButton: React.FC<Props> = ({ |
||||||
text, |
text, |
||||||
dataId, |
dataId, |
||||||
isSubmitting = false, |
isSubmitting = false, |
||||||
|
disable = true |
||||||
}) => { |
}) => { |
||||||
return ( |
return ( |
||||||
<button |
<CustomTooltip |
||||||
data-id={dataId} |
tooltipText={disable ? "Fill the fields with valid values" : "Click to proceed"} |
||||||
style={{ padding: "0.25rem 0.4rem", marginRight: "0.5em" }} |
tooltipId='etherscan-submit-button' |
||||||
type="submit" |
placement='bottom' |
||||||
className="btn btn-primary" |
|
||||||
disabled={isSubmitting} |
|
||||||
> |
> |
||||||
{!isSubmitting && text} |
<div> |
||||||
|
<button |
||||||
{isSubmitting && ( |
data-id={dataId} |
||||||
<div> |
style={{ padding: "0.25rem 0.4rem", marginRight: "0.5em" }} |
||||||
<span |
type="submit" |
||||||
className="spinner-border spinner-border-sm" |
className="btn btn-primary btn-block text-decoration-none" |
||||||
role="status" |
disabled={disable} |
||||||
aria-hidden="true" |
> |
||||||
style={{ marginRight: "0.3em" }} |
{!isSubmitting && text} |
||||||
/> |
{isSubmitting && ( |
||||||
Verifying... Please wait |
<div> |
||||||
</div> |
<span |
||||||
)} |
className="spinner-border spinner-border-sm" |
||||||
</button> |
role="status" |
||||||
|
aria-hidden="true" |
||||||
|
style={{ marginRight: "0.3em" }} |
||||||
|
/> |
||||||
|
Verifying... Please wait |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</CustomTooltip> |
||||||
) |
) |
||||||
} |
} |
||||||
|
@ -1,6 +1,9 @@ |
|||||||
export type ReceiptStatus = "Verified" | "Queue" |
export type ReceiptStatus = "Pending in queue" | "Pass - Verified" | "Already Verified" | "Max rate limit reached" | "Successfully Updated" |
||||||
|
|
||||||
export interface Receipt { |
export interface Receipt { |
||||||
guid: string |
guid: string |
||||||
status: ReceiptStatus |
status: ReceiptStatus |
||||||
|
isProxyContract: boolean |
||||||
|
message?: string |
||||||
|
succeed?: boolean |
||||||
} |
} |
||||||
|
@ -0,0 +1,36 @@ |
|||||||
|
export const scanAPIurls = { |
||||||
|
// all mainnet
|
||||||
|
1: "https://api.etherscan.io/api", |
||||||
|
56: "https://api.bscscan.com/api", |
||||||
|
137: "https://api.polygonscan.com/api", |
||||||
|
250: "https://api.ftmscan.com/api", |
||||||
|
42161: "https://api.arbiscan.io/api", |
||||||
|
43114: "https://api.snowtrace.io/api", |
||||||
|
1285: "https://api-moonriver.moonscan.io/api", |
||||||
|
1284: "https://api-moonbeam.moonscan.io/api", |
||||||
|
25: "https://api.cronoscan.com/api", |
||||||
|
199: "https://api.bttcscan.com/api", |
||||||
|
10: "https://api-optimistic.etherscan.io/api", |
||||||
|
42220: "https://api.celoscan.io/api", |
||||||
|
288: "https://api.bobascan.com/api", |
||||||
|
100: "https://api.gnosisscan.io/api", |
||||||
|
1101: "https://api-zkevm.polygonscan.com/api", |
||||||
|
|
||||||
|
// all testnet
|
||||||
|
5: "https://api-goerli.etherscan.io/api", |
||||||
|
11155111: "https://api-sepolia.etherscan.io/api", |
||||||
|
97: "https://api-testnet.bscscan.com/api", |
||||||
|
80001: "https://api-testnet.polygonscan.com/api", |
||||||
|
4002: "https://api-testnet.ftmscan.com/api", |
||||||
|
421611: "https://api-testnet.arbiscan.io/api", |
||||||
|
42170: "https://api-nova.arbiscan.io/api", |
||||||
|
43113: "https://api-testnet.snowtrace.io/api", |
||||||
|
1287: "https://api-moonbase.moonscan.io/api", |
||||||
|
338: "https://api-testnet.cronoscan.com/api", |
||||||
|
1028: "https://api-testnet.bttcscan.com/api", |
||||||
|
420: "https://api-goerli-optimistic.etherscan.io/api", |
||||||
|
44787: "https://api-alfajores.celoscan.io/api", |
||||||
|
2888: "https://api-testnet.bobascan.com/api", |
||||||
|
84531: "https://api-goerli.basescan.org/api", |
||||||
|
1442: "https://api-testnet-zkevm.polygonscan.com/api" |
||||||
|
} |
@ -1,25 +1,30 @@ |
|||||||
export const verifyScript = ` |
export const verifyScript = ` |
||||||
/** |
/** |
||||||
* @param {string} apikey - etherscan api key. |
* @param {string} apikey - etherscan api key |
||||||
* @param {string} contractAddress - Address of the contract to verify. |
* @param {string} contractAddress - Address of the contract to verify |
||||||
* @param {string} contractArguments - Parameters used in the contract constructor during the initial deployment. It should be the hex encoded value. |
* @param {string} contractArguments - Parameters used in the contract constructor during the initial deployment. It should be the hex encoded value |
||||||
* @param {string} contractName - Name of the contract |
* @param {string} contractName - Name of the contract |
||||||
* @param {string} contractFile - File where the contract is located |
* @param {string} contractFile - File where the contract is located |
||||||
|
* @param {number | string} chainRef - Network chain id or API URL (optional) |
||||||
|
* @param {boolean} isProxyContract - true, if contract is a proxy contract (optional) |
||||||
|
* @param {string} expectedImplAddress - Implementation contract address, in case of proxy contract verification (optional) |
||||||
* @returns {{ guid, status, message, succeed }} verification result |
* @returns {{ guid, status, message, succeed }} verification result |
||||||
*/ |
*/ |
||||||
export const verify = async (apikey: string, contractAddress: string, contractArguments: string, contractName: string, contractFile: string) => { |
export const verify = async (apikey: string, contractAddress: string, contractArguments: string, contractName: string, contractFile: string, chainRef?: number | string, isProxyContract?: boolean, expectedImplAddress?: string) => { |
||||||
const compilationResultParam = await remix.call('compilerArtefacts' as any, 'getCompilerAbstract', contractFile) |
const compilationResultParam = await remix.call('compilerArtefacts' as any, 'getCompilerAbstract', contractFile) |
||||||
console.log('verifying.. ' + contractName) |
console.log('verifying.. ' + contractName) |
||||||
return await remix.call('etherscan' as any, 'verify', apikey, contractAddress, contractArguments, contractName, compilationResultParam) |
// update apiKey and chainRef to verify contract on multiple networks
|
||||||
|
return await remix.call('etherscan' as any, 'verify', apikey, contractAddress, contractArguments, contractName, compilationResultParam, chainRef, isProxyContract, expectedImplAddress) |
||||||
}` |
}` |
||||||
|
|
||||||
export const receiptGuidScript = ` |
export const receiptGuidScript = ` |
||||||
/** |
/** |
||||||
* @param {string} apikey - etherscan api key. |
* @param {string} apikey - etherscan api key |
||||||
* @param {string} guid - receipt id. |
* @param {string} guid - receipt id |
||||||
|
* @param {boolean} isProxyContract - true, if contract is a proxy contract (optional) |
||||||
* @returns {{ status, message, succeed }} receiptStatus |
* @returns {{ status, message, succeed }} receiptStatus |
||||||
*/ |
*/ |
||||||
export const receiptStatus = async (apikey: string, guid: string) => { |
export const receiptStatus = async (apikey: string, guid: string, isProxyContract?: boolean) => { |
||||||
return await remix.call('etherscan' as any, 'receiptStatus', guid, apikey) |
return await remix.call('etherscan' as any, 'receiptStatus', guid, apikey, isProxyContract) |
||||||
} |
} |
||||||
` |
` |
@ -0,0 +1,16 @@ |
|||||||
|
{ |
||||||
|
"name": "etherscan", |
||||||
|
"displayName": "Contract verification - Etherscan", |
||||||
|
"description": "Verify Solidity contract code using Etherscan, BscScan, PolygonScan etc. APIs", |
||||||
|
"version": "0.1.0", |
||||||
|
"events": [], |
||||||
|
"methods": ["verify", "receiptStatus"], |
||||||
|
"kind": "none", |
||||||
|
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHsAAAB7CAYAAABUx/9/AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAHBklEQVR42u2dy3WbTBTH/+ikgFEFwZ8K8HQAm6yjDkIHdiogHWBXgDqADgQVaLLJToZ1NtAB38JC0QNJIPGYx73nsIgcHVvz033OnblWVVXQQcqyhBACQgiUZYk0TfHnz5/q79+/rd7PGPsA8I1z/gEAjuOAMQbOOVzX1WKNLFVhCyGQJAnSNEUcx0N/CMu2bXDO4TgOXNcF51y9RauqSomnKApEUQTG2BZANeXDGNsyxuB5HqIoUmYNpf8DoyjC1HBbPEqAl/KPyrJMCg2+V+N930eWZQT72rNer6Ea4FvavtlsCLbGkM+gu66L9XptNuwsy3SG3KjpU5r3ySJrgyCfQfd93wzYmpvs1sBt2x7dtJM2S2DatYJN2nxby8eI2of/BQSzNfQgCNSETWb7PuDL5VIt2IalVL0D55wPwmXW98bKfD7fPj096bFvOtHeVJ7n2/l8DiGEvLteKtazZa+z9xm4EWiDgBNog4ATaIOAE2iDgFMebVBadnfDoWVZlF5N1CTKOcdms+n8xhmBVi8PF0Lg58+fw+fZZErlMeldGxwpIFM8YOvS+UJarUF/W++waWGbNSuKoof6ynpom269NUrme0SfOYQydTHnpNUPwO6zU3QMc06gH1hgyTp6blqam0UVyqkvL02fJ2B7WGfLtu26caR7UYVAK9f0gLe3t+7fzvl8vi3L8j9aQ2U0u75QYLHbt2hfQSOfrJzPvnni5OK3k0y4epp9y3fPCLSevnu1WrX7dhJspTX74jbojEDruw162oo8o3XRV97f3y+bIkq3tDHjzWmYhukWPM976Oxzy50oFQ5AIgzD5to4DG/I6whACdiHBwVnhyZcB9uq5M2DAwljbJskyXmApouv/vr1K1E+YFqWJeI4pmjcFEnTlGCbInmeAwC+UCFFa5/9AaC+//UTNom2PntxWDIlM65x0ScMw6PshGBrCtr3fXied/QiwdYQtOd5+PXr19kPZhSc6QXadV2EYdj4wykCNItzDs555wLI+/s7bdTcWNfdbZIXRam7Om9sUPR64y/UqY232hMY3Wf/+PHjofcXRbEgJT7X6DaH80eHrcuMLNVAUzRuEGiCbRBogm0Q6Le3N6qNmwB6V0uxSLMVK5jcCZrMuEqg6ybKLnLaakawFQDt+/7FEug10KfVRvLZkkrd9x0EwdnuVRtpKisTbIlBr9fru7plL21uEWwJzXY9+umuN1/ZxSSfLRno19fXQUCTZkskVVVZcRxjuVwOApo0WzIZEjTB1sHud+g0mlVVZdGSaQ/a4pyTZpug0cDncFeCbQBoAHh+fibYJoAGPjuECLYikuf5Q2fyOOefeXZVVRb1j8srD951Y3HOwRgjzdYc9N6EA1RB09I/n4rjOJ95dv1CfZaXRB/QjLEPxti+MreHTc33+ml0WZaLwz598tl3sPA8z7p2Y/9UEXeTfP/+/d8/Rp7yM/S5qCHPesHzvF6HwAy59vVUoKIo9r/jKEArimJBKdixzwOweHl5aTzvLJvZPjXhnueBMbZ/jaLxKzJGHDOkcp0eojzz2bQLNo4kSTIkaItzfnaIkjRb4Wj7mry8vJy91hiNk3YPI0NE203fJdu2G9uPSbNHkjHvcr904cGXa5Eo3V+ijtk+yB7w+vraDbZqaVj9QW3bNlKb63QrCIKjdOvoSzfyLM5eRi3UR1Bt2wbnHI7jwHVdae4az/McT09PYyvKzVmcSvrsLMss2bR4Km0+lCAIrv+HsUcHDlFqlOHZHaedbFxGL/Oze67bagd5ZzYnm4tS18Db1OxbmfGiKBZxHO/nRDHG8Pz8fC1YOvKftm1LaXJVibJvBWW+77da315nU1HOPP73zXXd1jcy0H52B1mtVrAsq5IBdN2F0ulGBl0Dpj6f3YJKN8PscEBbq0CbYCoHuaqrZF0/D/lsuX1yo5++5y408tknVa/5fL6VxSdfq5J1vSKLfLb8proxn95sNvf31JkIuOUUXa1AGwd7VztWanJwX6CNgK2KmR4atJawsyxTUoOHBq0N7CiK6u6MSodnCNDKwtYNLhom+Qxx8kT65oUkSSCEwO/fv7FarXSvAFnL5RJhGF5sLXpEpICd5znyPIcQAmVZIk1TJEliWmnPCoLgYrOgUrCTJEGSJAD+TWo3EOjFqlgYhsOPwRrT12rqYx+eYHh40nLQ9TesV0sayHWNe1RlmyqiNhm07/ujabM0qdfUzXpTmOyhDvMrk2fv9ma1hVz3iU29ztSWO7Am910F066ClmUZfN9XdofK9/1JzbWy5dIoilSAjuVyiSiKpF5L5WriMjQd1BrseR6iKJoksr7nUbbhUAiBJEmQpiniOB7lNgMZT4x2+hA6dZfWmyZ1fV0I0bpLtL4Gq4boOM7+GFN9q6/q8j8QmqQtM04gOgAAAABJRU5ErkJggg==", |
||||||
|
"location": "sidePanel", |
||||||
|
"url": "https://ipfs-cluster.ethdevops.io/ipfs/QmQsZbBSYCVBVpz2mVRbPRVTrcz59oJEpuuoxiT9otu3mh", |
||||||
|
"repo": "https://github.com/ethereum/remix-project/tree/master/apps/etherscan", |
||||||
|
"documentation": "https://remix-etherscan-plugin.readthedocs.io/en/latest", |
||||||
|
"maintainedBy": "Remix", |
||||||
|
"authorContact": "remix@ethereum.org" |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
{ |
||||||
|
"name": "remix-ide-e2e", |
||||||
|
"license": "MIT", |
||||||
|
"engines": { |
||||||
|
"node": "^20.0.0", |
||||||
|
"npm": "^6.14.15" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"@openzeppelin/contracts": "^4.8.3", |
||||||
|
"@openzeppelin/contracts-upgradeable": "^4.8.3", |
||||||
|
"@openzeppelin/upgrades-core": "^1.22.0", |
||||||
|
"@openzeppelin/wizard": "^0.1.1", |
||||||
|
"@remix-project/remixd": "../../dist/libs/remixd", |
||||||
|
"deep-equal": "^1.0.1", |
||||||
|
"ganache-cli": "^6.8.1", |
||||||
|
"selenium-standalone": "^8.2.3", |
||||||
|
"tree-kill": "^1.2.2" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"http-server": "^14.1.1", |
||||||
|
"nightwatch": "2.3" |
||||||
|
} |
||||||
|
} |
@ -1,4 +1,9 @@ |
|||||||
{ |
{ |
||||||
"presets": ["@nrwl/react/babel"], |
"presets": ["@babel/preset-env", ["@babel/preset-react", |
||||||
"plugins": [] |
{"runtime": "automatic"} |
||||||
|
]], |
||||||
|
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime", "@babel/plugin-proposal-nullish-coalescing-operator"], |
||||||
|
"ignore": [ |
||||||
|
"**/node_modules/**" |
||||||
|
] |
||||||
} |
} |
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue