Skip to content

Commit

Permalink
feat: generate node config type (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
spsjvc authored Feb 23, 2024
1 parent 27b1a41 commit 65784be
Show file tree
Hide file tree
Showing 7 changed files with 1,356 additions and 82 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"build": "tsc --project ./tsconfig.json --module commonjs --outDir ./src/dist --declaration",
"dev": "yarn build --watch",
"generate": "wagmi generate",
"generate:node-config-type": "yarn build && node ./src/dist/scripts/generateNodeConfigType.js",
"postgenerate:node-config-type": "prettier --write ./src/types/NodeConfig.generated.ts",
"test:unit": "vitest unit.test",
"test:integration": "vitest integration.test",
"postinstall": "patch-package",
Expand All @@ -23,6 +25,7 @@
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.3",
"ts-morph": "^21.0.1",
"typescript": "^5.2.2",
"vitest": "^0.34.6"
}
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ import {
import { ChainConfig, ChainConfigArbitrumParams } from './types/ChainConfig';
import { CoreContracts } from './types/CoreContracts';
import { ParentChain, ParentChainId } from './types/ParentChain';
import { NodeConfig, NodeConfigChainInfoJson } from './types/NodeConfig';
import { NodeConfig } from './types/NodeConfig.generated';
import { NodeConfigChainInfoJson } from './types/NodeConfig';
import { prepareNodeConfig } from './prepareNodeConfig';
import {
createTokenBridgeEnoughCustomFeeTokenAllowance,
Expand Down
10 changes: 5 additions & 5 deletions src/prepareNodeConfig.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NodeConfig } from './types/NodeConfig.generated';
import {
NodeConfig,
NodeConfigChainInfoJson,
NodeConfigDataAvailabilityRpcAggregatorBackendsJson,
} from './types/NodeConfig';
Expand Down Expand Up @@ -100,8 +100,8 @@ export function prepareNodeConfig({
'http': {
addr: '0.0.0.0',
port: 8449,
vhosts: '*',
corsdomain: '*',
vhosts: ['*'],
corsdomain: ['*'],
api: ['eth', 'net', 'web3', 'arb', 'debug'],
},
'node': {
Expand Down Expand Up @@ -143,13 +143,13 @@ export function prepareNodeConfig({
};

if (chainConfig.arbitrum.DataAvailabilityCommittee) {
config.node['data-availability'] = {
config.node!['data-availability'] = {
'enable': true,
'sequencer-inbox-address': coreContracts.sequencerInbox,
'parent-chain-node-url': parentChainRpcUrl,
'rest-aggregator': {
enable: true,
urls: 'http://localhost:9877',
urls: ['http://localhost:9877'],
},
'rpc-aggregator': {
'enable': true,
Expand Down
201 changes: 201 additions & 0 deletions src/scripts/generateNodeConfigType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { execSync } from 'child_process';
import { readFileSync, rmSync } from 'fs';
import { Project, WriterFunction, Writers } from 'ts-morph';

const { objectType } = Writers;

function getNitroNodeImageTag(): string {
const defaultNitroNodeTag = 'v2.2.5-a20a1c7';
const argv = process.argv.slice(2);

if (argv.length < 2 || argv[0] !== '--nitro-node-tag') {
console.log(
`Using default nitro-node tag since none was provided. If you want to specify a tag, you can do "--nitro-node-tag v2.2.2-8f33fea".`,
);
return defaultNitroNodeTag;
}

return argv[1];
}

const nitroNodeTag = getNitroNodeImageTag();
const nitroNodeImage = `offchainlabs/nitro-node:${nitroNodeTag}`;
const nitroNodeHelpOutputFile = `${nitroNodeImage.replace('/', '-')}-help.txt`;

console.log(`Using image "${nitroNodeImage}".`);

function generateHeader() {
return [
`// ---`,
`//`,
`// THIS FILE IS AUTOMATICALLY GENERATED AND SHOULD NOT BE EDITED MANUALLY`,
`//`,
`// IMAGE: ${nitroNodeImage}`,
`// TIMESTAMP: ${new Date().toISOString()}`,
`// `,
`// ---`,
].join('\n');
}

type CliOption = {
name: string;
type: string;
docs: string[];
};

function parseCliOptions(fileContents: string): CliOption[] {
const types: Record<string, string | undefined> = {
string: 'string',
strings: 'string[]',
stringArray: 'string[]',
int: 'number',
uint: 'number',
uint32: 'number',
float: 'number',
boolean: 'boolean',
duration: 'string',
};

// split into lines
let lines = fileContents.split('\n');
// trim whitespaces
lines = lines.map((line) => line.trim());
// special case for flags with comments that span multiple lines
// todo: generalize this so it automatically detects and merges multi-line comments??
lines = lines.map((line, lineIndex) => {
// this comment spans 3 lines
if (line.includes('max-fee-cap-formula')) {
return `${line} ${lines[lineIndex + 1]} ${lines[lineIndex + 2]}`;
}

return line;
});
// only leave lines that start with "--", e.g. "--auth.addr string" but exclude "--help" and "--dev"

lines = lines.filter(
(line) => line.startsWith('--') && !line.includes('--help') && !line.includes('--dev'),
);
// sanitize the boolean types
lines = lines.map((line) => {
let split = line.split(' ');
// if the flag is just a boolean, then the type is omitted from the --help output, e.g. "--init.force"
// to make things simpler and consistent, we replace the empty string with boolean
if (split[1] === '') {
split[1] = 'boolean';
}

return split.join(' ');
});

return lines.map((line) => {
const [name, type] = line.split(' ');

// get the mapped type from go to typescript
const sanitizedType = types[type];

// docs is everything after the param name and type (and one space in between)
const docsStart = name.length + 1 + type.length;
const docs = line.slice(docsStart).trim();

if (typeof sanitizedType === 'undefined') {
throw new Error(`Unknown type: ${type}`);
}

return {
// remove "--" from the name
name: name.replace('--', ''),
// map the go type to the typescript type
type: sanitizedType,
// copy the rest of the line as docs
docs: [docs],
};
});
}

type CliOptionNestedObject = {
[key: string]: CliOption | CliOptionNestedObject;
};

function createCliOptionsNestedObject(options: CliOption[]): CliOptionNestedObject {
const result: CliOptionNestedObject = {};

options.forEach((option) => {
let paths = option.name.split('.');
let current: CliOptionNestedObject = result;

for (let i = 0; i < paths.length; i++) {
const path = paths[i];
const pathIsFinal = i === paths.length - 1;

if (typeof current[path] === 'undefined') {
current[path] = pathIsFinal ? option : {};
}

current = current[path] as CliOptionNestedObject;
}
});

return result;
}

function isCliOption(value: CliOption | CliOptionNestedObject): value is CliOption {
return 'type' in value;
}

function getDocs(value: CliOption | CliOptionNestedObject): string[] {
if (isCliOption(value)) {
return value.docs;
}

// docs only available for "primitive" properties, not objects
return [];
}

function getTypeRecursively(value: CliOption | CliOptionNestedObject): string | WriterFunction {
// if we reached the "primitive" property, we can just return its type
if (isCliOption(value)) {
return value.type;
}

// if not, recursively figure out the type for each of the object's properties
return objectType({
properties: Object.entries(value).map(([currentKey, currentValue]) => ({
name: `'${currentKey}'`,
type: getTypeRecursively(currentValue),
docs: getDocs(currentValue),
// make it optional
hasQuestionToken: true,
})),
});
}

function main() {
// run --help on the nitro binary and save the output to a file
execSync(`docker run --rm ${nitroNodeImage} --help > ${nitroNodeHelpOutputFile} 2>&1`);

// read and parse the file
const content = readFileSync(nitroNodeHelpOutputFile, 'utf8');
const cliOptions = parseCliOptions(content);
const cliOptionsNestedObject = createCliOptionsNestedObject(cliOptions);

// create the new source file
const sourceFile = new Project().createSourceFile('./src/types/NodeConfig.generated.ts', '', {
overwrite: true,
});
// append header
sourceFile.insertText(0, generateHeader());
// append NodeConfig type declaration
sourceFile.addTypeAlias({
name: 'NodeConfig',
type: getTypeRecursively(cliOptionsNestedObject),
docs: ['Nitro node configuration options'],
isExported: true,
});

// save file to disk
sourceFile.saveSync();
// remove output file that we used for parsing
rmSync(nitroNodeHelpOutputFile);
}

main();
Loading

0 comments on commit 65784be

Please sign in to comment.