o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
307 lines (281 loc) • 8.21 kB
JavaScript
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import prettier from 'prettier';
import prettierRc from '../../.prettierrc.cjs';
// let jsLayout = JSON.parse(process.argv[2]);
let selfPath = fileURLToPath(import.meta.url);
let jsonPath = path.resolve(selfPath, '../../bindings/ocaml/jsLayout.json');
let jsLayout = JSON.parse(await fs.readFile(jsonPath, 'utf8'));
console.log(`js-layout-to-types.mjs: generating TS types from ${jsonPath}`);
let builtinLeafTypes = new Set([
'number',
'string',
'null',
'undefined',
'bigint',
]);
let indent = '';
function writeType(typeData, isValue, isJson, withTypeMap) {
let converters = {};
if (!(isJson || isValue) && typeData.checkedType) {
let name = typeData.checkedTypeName;
converters[name] = {
typeName: name,
type: writeType(typeData.checkedType, false, false, withTypeMap).output,
valueType: writeType(typeData.checkedType, true, false, true).output,
jsonType: writeType(typeData, false, true, true).output,
};
typeData = typeData.checkedType;
}
let { type, inner, entries, keys, optionType, docEntries } = typeData;
if (type === 'array') {
let {
output,
dependencies,
converters: j,
} = writeType(inner, isValue, isJson, withTypeMap);
mergeObject(converters, j);
return {
output: `${output}[]`,
dependencies,
converters,
};
}
if (type === 'option') {
let {
output,
dependencies,
converters: j,
} = writeType(inner, isValue, isJson, withTypeMap);
if (optionType === 'flaggedOption' || optionType === 'closedInterval') {
dependencies ??= new Set();
dependencies.add('Bool');
}
mergeObject(converters, j);
return {
output: isJson
? `(${output} | null)`
: optionType === 'implicit'
? output
: optionType === 'flaggedOption' || optionType === 'closedInterval'
? `{isSome: Bool, value: ${output}}`
: `(${output} | undefined)`,
dependencies,
converters,
};
}
if (type === 'object') {
let dependencies = new Set();
let output = '{\n';
indent += ' ';
// TODO: make docs work and use them for doccomments
for (let key of keys) {
let value = entries[key];
let questionMark = '';
if (
!isJson &&
value.type === 'option' &&
value.optionType === 'orUndefined'
) {
value = value.inner;
questionMark = '?';
}
let inner = writeType(value, isValue, isJson, withTypeMap);
mergeSet(dependencies, inner.dependencies);
mergeObject(converters, inner.converters);
output += indent + `${key}${questionMark}: ${inner.output};\n`;
}
indent = indent.slice(2);
output += indent + '}';
return { output, dependencies, converters };
}
if (withTypeMap && !builtinLeafTypes.has(type)) {
type = `${isJson ? 'Json.' : isValue ? 'Value.' : ''}TypeMap["${type}"]`;
}
// built in type
if (builtinLeafTypes.has(type)) return { output: type, converters };
// else: leaf type that has to be specified manually
return {
output: type,
dependencies: builtinLeafTypes.has(type) ? new Set() : new Set([type]),
converters,
};
}
function writeTsContent({
jsLayout: types,
isJson,
isProvable,
leavesRelPath,
}) {
let output = '';
let dependencies = new Set();
let converters = {};
let exports = new Set(isJson ? [] : ['customTypes']);
let fromLayout = isProvable ? 'provableFromLayout' : 'signableFromLayout';
let FromLayout = isProvable ? 'ProvableFromLayout' : 'SignableFromLayout';
let Type = (Name, T, TValue, ...args) =>
`${Name}<${(isProvable ? [T, TValue, ...args] : [T, ...args]).join(', ')}>`;
let GenericType = isProvable ? 'GenericProvableExtended' : 'GenericSignable';
let GeneratedType = isProvable ? 'ProvableExtended' : 'Signable';
for (let [T, value] of Object.entries(types)) {
let inner = writeType(value, false, isJson);
exports.add(T);
mergeSet(dependencies, inner.dependencies);
mergeObject(converters, inner.converters);
output += `type ${T} = ${inner.output};\n\n`;
if (!isJson) {
output += `let ${T} = ${Type(
fromLayout,
T,
`Value.${T}`,
`Json.${T}`
)}(jsLayout.${T} as any);\n\n`;
}
}
let customTypes = Object.values(converters);
let customTypeNames = Object.values(converters).map((c) => c.typeName);
let imports = new Set();
mergeSet(imports, dependencies);
mergeSet(imports, new Set(customTypeNames));
let typeMapKeys = diffSets(dependencies, new Set(customTypeNames));
let importPath = leavesRelPath;
return `// @generated this file is auto-generated - don't edit it directly
import { ${[...imports].join(', ')} } from '${importPath}';
${
!isJson
? `import { ${GenericType} } from '../../lib/generic.js';\n` +
`import { ${FromLayout}, GenericLayout } from '../../lib/from-layout.js';\n` +
"import * as Json from './transaction-json.js';\n" +
(isProvable
? "import * as Value from './transaction-bigint.js';\n"
: '') +
"import { jsLayout } from './js-layout.js';\n"
: ''
}
export { ${[...exports].join(', ')} };
${
!isJson
? 'export { Json };\n' +
`export * from '${leavesRelPath}';\n` +
`export { ${fromLayout}, toJSONEssential, empty, Layout, TypeMap };\n`
: `export * from '${leavesRelPath}';\n` + 'export { TypeMap };\n'
}
type TypeMap = {
${[...typeMapKeys].map((type) => `${type}: ${type};`).join('\n')}
}
${
(!isJson || '') &&
`
const TypeMap: {
[K in keyof TypeMap]: ${Type(
GeneratedType,
'TypeMap[K]',
'Value.TypeMap[K]',
'Json.TypeMap[K]'
)};
} = {
${[...typeMapKeys].join(', ')}
}
`
}
${
(!isJson || '') &&
`
type ${Type(GeneratedType, 'T', 'TValue', 'TJson')} = ${Type(
GenericType,
'T',
'TValue',
'TJson',
'Field'
)};
type Layout = GenericLayout<TypeMap>;
type CustomTypes = { ${customTypes
.map(
(c) =>
`${c.typeName}: ${Type(
GeneratedType,
c.type,
c.valueType,
c.jsonType
)};`
)
.join(' ')} }
let customTypes: CustomTypes = { ${customTypeNames.join(', ')} };
let { ${fromLayout}, toJSONEssential, empty } = ${Type(
FromLayout,
'TypeMap',
'Value.TypeMap',
'Json.TypeMap'
)}(TypeMap, customTypes);
`
}
${output}`;
}
async function writeTsFile(content, relPath) {
let absPath = path.resolve(selfPath, relPath);
content = prettier.format(content, {
filepath: absPath,
...prettierRc,
});
await fs.writeFile(absPath, content);
}
let genPath = '../../bindings/mina-transaction/gen';
await ensureDir(genPath);
let jsonTypesContent = writeTsContent({
jsLayout,
isJson: true,
leavesRelPath: '../transaction-leaves-json.js',
});
await writeTsFile(jsonTypesContent, `${genPath}/transaction-json.ts`);
let jsTypesContent = writeTsContent({
jsLayout,
isJson: false,
isProvable: true,
leavesRelPath: '../transaction-leaves.js',
});
await writeTsFile(jsTypesContent, `${genPath}/transaction.ts`);
jsTypesContent = writeTsContent({
jsLayout,
isJson: false,
isProvable: false,
leavesRelPath: '../transaction-leaves-bigint.js',
});
await writeTsFile(jsTypesContent, `${genPath}/transaction-bigint.ts`);
await writeTsFile(
`// @generated this file is auto-generated - don't edit it directly
export { jsLayout };
let jsLayout = ${JSON.stringify(jsLayout)};
`,
`${genPath}/js-layout.ts`
);
function mergeSet(target, source) {
if (source === undefined) return;
for (let x of source) {
target.add(x);
}
}
function diffSets(s0, s1) {
let s = new Set(s0);
for (let x of s1) {
s.delete(x);
}
return s;
}
function mergeObject(target, source) {
if (source === undefined) return;
for (let key in source) {
target[key] = source[key];
}
}
async function ensureDir(relPath) {
let absPath = path.resolve(selfPath, relPath);
let exists = false;
try {
await fs.stat(absPath);
exists = true;
} catch {}
if (!exists) {
await fs.mkdir(absPath, { recursive: true });
}
}