Skip to content

Commit a8bd99a

Browse files
authored
Improve @metamask/delegation-abis tree-shakability (#131)
* Split ABIs and bytecode, enabling tree-shaking per contract; does _not_ update generated files - remove bytecodes from default export @metamask/delegation-abis - add new export @metamask/delegation-abis/bytecode - shuffle src files into src/abis, src/bytecode and src/artifacts - updates scripts/format-abis.js to ESM - disable bundling in tsup to avoid large chunks - adds plugin to rewrite relative import file extensions * Update generated files * Update deploySmartAccountsEnvironment to use new abi imports * Update all references to @metamask/delegation-abis that now import only the abis to match new interface * Remove redundant NarrowAbiToFunction utility type * Update e2e test suite to use new interface
1 parent 11a7b92 commit a8bd99a

File tree

444 files changed

+14341
-14188
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

444 files changed

+14341
-14188
lines changed

packages/delegation-abis/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@
3737
"default": "./dist/index.mjs"
3838
}
3939
},
40+
"./bytecode": {
41+
"require": {
42+
"types": "./dist/bytecode.d.cts",
43+
"default": "./dist/bytecode.cjs"
44+
},
45+
"import": {
46+
"types": "./dist/bytecode.d.ts",
47+
"default": "./dist/bytecode.mjs"
48+
}
49+
},
4050
"./package.json": "./package.json"
4151
},
4252
"engines": {
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// From https://github.com/aymericzip/esbuild-fix-imports-plugin/blob/main/src/fixExtensionsPlugin.ts, included here to avoid adding a dependency
2+
import type { Plugin } from 'esbuild';
3+
import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
4+
import { join, extname } from 'node:path';
5+
6+
const CJS_RELATIVE_IMPORT_EXP = /require\s*\(\s*["'](\..+?)["']\s*\)(;?)/g;
7+
const ESM_RELATIVE_IMPORT_EXP = /from\s*(["'])(\.[^"']+)\1([^;]*;?)/g;
8+
const hasJSExtensionRegex = /\.(?:js)$/i;
9+
const hasNonJSExtensionRegex =
10+
/\.(?:png|svg|css|scss|csv|tsv|xml|toml|ini|jpe?g|json|md|mdx|yaml|gif|webp|ico|mp4|webm|ogg|wav|mp3|m4a|aac|webm|woff2?|eot|ttf|otf|wasm)$/i;
11+
12+
function modifyEsmImports(contents: string, outExt: string) {
13+
return contents.replace(ESM_RELATIVE_IMPORT_EXP, (_m, quote, importPath, rest = '') => {
14+
if (importPath.endsWith('.') || importPath.endsWith('/')) {
15+
return `from ${quote}${importPath}/index${outExt}${quote}${rest}`;
16+
}
17+
if (importPath.endsWith(outExt)) {
18+
return `from ${quote}${importPath}${quote}${rest}`;
19+
}
20+
if (hasJSExtensionRegex.test(importPath) && outExt !== '.js') {
21+
return `from ${quote}${importPath.replace(hasJSExtensionRegex, outExt)}${quote}${rest}`;
22+
}
23+
if (hasNonJSExtensionRegex.test(importPath)) {
24+
return `from ${quote}${importPath}${quote}${rest}`;
25+
}
26+
return `from ${quote}${importPath}${outExt}${quote}${rest}`;
27+
});
28+
}
29+
30+
function modifyCjsImports(contents: string, outExt: string) {
31+
return contents.replace(CJS_RELATIVE_IMPORT_EXP, (_m, importPath, maybeSemi = '') => {
32+
if (importPath.endsWith('.') || importPath.endsWith('/')) {
33+
return `require('${importPath}/index${outExt}')${maybeSemi}`;
34+
}
35+
if (hasJSExtensionRegex.test(importPath) && outExt !== '.js') {
36+
return `require('${importPath.replace(hasJSExtensionRegex, outExt)}')${maybeSemi}`;
37+
}
38+
if (importPath.endsWith(outExt) || hasNonJSExtensionRegex.test(importPath)) {
39+
return `require('${importPath}')${maybeSemi}`;
40+
}
41+
return `require('${importPath}${outExt}')${maybeSemi}`;
42+
});
43+
}
44+
45+
function modifyRelativeImports(contents: string, isEsm: boolean, outExt: string) {
46+
return isEsm ? modifyEsmImports(contents, outExt) : modifyCjsImports(contents, outExt);
47+
}
48+
49+
// Fallback if esbuild doesn't provide outputFiles (write: true)
50+
function processOutdirFiles(outdir: string, isEsm: boolean, outExt: string) {
51+
const stack = [outdir];
52+
while (stack.length) {
53+
const dir = stack.pop()!;
54+
for (const entry of readdirSync(dir)) {
55+
const p = join(dir, entry);
56+
if (statSync(p).isDirectory()) {
57+
stack.push(p);
58+
continue;
59+
}
60+
const ext = extname(p);
61+
if (ext !== outExt) {
62+
continue;
63+
}
64+
if (ext === '.mjs' || ext === '.cjs' || ext === '.js') {
65+
const original = readFileSync(p, 'utf8');
66+
const updated = modifyRelativeImports(original, isEsm || ext === '.mjs', outExt);
67+
if (updated !== original) {
68+
writeFileSync(p, updated);
69+
}
70+
}
71+
}
72+
}
73+
}
74+
75+
export const fixExtensionsPlugin = (): Plugin => ({
76+
name: 'fixExtensionsPlugin',
77+
setup(build) {
78+
const isEsm = build.initialOptions.format === 'esm';
79+
const outExt = build.initialOptions.outExtension?.['.js'] ?? (isEsm ? '.mjs' : '.cjs');
80+
const outdir = build.initialOptions.outdir;
81+
82+
build.onEnd((result) => {
83+
if (result.errors.length > 0) return;
84+
85+
if (result.outputFiles && result.outputFiles.length > 0) {
86+
for (const ofile of result.outputFiles) {
87+
if (!ofile.path.endsWith(outExt)) continue;
88+
const next = modifyRelativeImports(ofile.text, isEsm, outExt);
89+
ofile.contents = Buffer.from(next);
90+
}
91+
} else if (outdir) {
92+
processOutdirFiles(outdir, isEsm, outExt);
93+
}
94+
});
95+
},
96+
});
97+
Lines changed: 100 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,133 @@
1-
const fs = require('fs');
2-
const path = require('path');
1+
import fs from 'node:fs';
2+
import path from 'node:path';
33

44
// Directory containing the JSON ABI files
5-
const inputDir = './src/raw';
6-
// Output directory for the TypeScript files
7-
const outputDir = './src/formatted';
5+
const inputDir = './src/artifacts';
6+
const abiDir = './src/abis';
7+
const bytecodeModulesDir = './src/bytecode';
8+
const INDEX_FILE = './src/index.ts';
9+
const BYTECODE_FILE = './src/bytecode.ts';
810

9-
// Ensure the output directory exists
10-
if (!fs.existsSync(outputDir)) {
11-
fs.mkdirSync(outputDir, { recursive: true });
11+
if (!fs.existsSync(abiDir)) {
12+
fs.mkdirSync(abiDir, { recursive: true });
13+
}
14+
if (!fs.existsSync(bytecodeModulesDir)) {
15+
fs.mkdirSync(bytecodeModulesDir, { recursive: true });
1216
}
1317

14-
// Export ts in an index file
15-
function addExportToIndexFile(fileName) {
16-
const indexStream = fs.createWriteStream('./src/index.ts', {
17-
flags: 'a',
18-
});
18+
// Initialize index files (truncate)
19+
fs.writeFileSync(INDEX_FILE, '');
20+
fs.writeFileSync(BYTECODE_FILE, '');
1921

20-
indexStream.write(`export * as ${fileName} from './formatted/${fileName}'\n`);
22+
// Write exports to index and bytecode files in sorted order
23+
function writeIndexFiles(exportNames) {
24+
const sortedNames = exportNames
25+
.slice()
26+
.sort((a, b) => a.localeCompare(b));
27+
const indexContents = sortedNames
28+
.map((fileName) => `export { abi as ${fileName} } from './abis/${fileName}';`)
29+
.join('\n');
30+
const bytecodeContents = sortedNames
31+
.map(
32+
(fileName) => `export { bytecode as ${fileName} } from './bytecode/${fileName}';`,
33+
)
34+
.join('\n');
2135

22-
indexStream.end();
36+
fs.writeFileSync(INDEX_FILE, `${indexContents}\n`);
37+
fs.writeFileSync(BYTECODE_FILE, `${bytecodeContents}\n`);
2338
}
2439

2540
// Recursive function to process files and directories
2641
function processDirectory(directory) {
27-
fs.readdir(directory, { withFileTypes: true }, (err, entries) => {
28-
if (err) {
29-
console.error('Error reading directory:', err);
30-
return;
31-
}
42+
const exportNames = [];
43+
let entries;
44+
try {
45+
entries = fs.readdirSync(directory, { withFileTypes: true });
46+
} catch (err) {
47+
console.error('Error reading directory:', err);
48+
return exportNames;
49+
}
3250

33-
const ignoreList = ['utils', 'build-info'];
34-
entries.forEach((entry) => {
51+
const ignoreList = ['utils', 'build-info'];
52+
entries
53+
.slice()
54+
.sort((a, b) => a.name.localeCompare(b.name))
55+
.forEach((entry) => {
3556
if (!ignoreList.includes(path.basename(directory))) {
3657
const fullPath = path.join(directory, entry.name);
3758
if (entry.isDirectory()) {
3859
// Recurse into subdirectories
39-
processDirectory(fullPath);
60+
exportNames.push(...processDirectory(fullPath));
4061
} else if (path.extname(entry.name) === '.json') {
4162
// Process JSON files
42-
processFile(fullPath, entry.name);
63+
const exportName = processFile(fullPath, entry.name);
64+
if (exportName) {
65+
exportNames.push(exportName);
66+
}
4367
}
4468
}
4569
});
46-
});
70+
71+
return exportNames;
4772
}
4873

4974
// Function to process each JSON file
5075
function processFile(filePath, fileName) {
51-
fs.readFile(filePath, 'utf8', (err, data) => {
52-
if (err) {
53-
console.error(`Error reading file ${fileName}:`, err);
54-
return;
55-
}
76+
let data;
77+
try {
78+
data = fs.readFileSync(filePath, 'utf8');
79+
} catch (err) {
80+
console.error(`Error reading file ${fileName}:`, err);
81+
return null;
82+
}
5683

57-
try {
58-
const abi = JSON.parse(data);
59-
const tsContent = `export const abi = ${JSON.stringify(
60-
abi.abi,
61-
null,
62-
2,
63-
)} as const;\n
84+
try {
85+
const parsed = JSON.parse(data);
86+
const abi = parsed.abi;
87+
const bytecode = parsed.bytecode?.object ?? '';
88+
const abiOnlyContent = `export const abi = ${JSON.stringify(
89+
abi,
90+
null,
91+
2,
92+
)} as const;\n`;
93+
const bytecodeOnlyContent = `export const bytecode = '${bytecode}' as const;`;
94+
const abiOnlyPath = path.join(
95+
abiDir,
96+
`${path.basename(fileName, '.json')}.ts`,
97+
);
98+
const bytecodeOnlyPath = path.join(
99+
bytecodeModulesDir,
100+
`${path.basename(fileName, '.json')}.ts`,
101+
);
64102

65-
export const bytecode = \"${abi.bytecode.object}\" as const;`;
66-
const outputFilePath = path.join(
67-
outputDir,
68-
`${path.basename(fileName, '.json')}.ts`,
103+
// Write abi-only and bytecode-only modules
104+
const exportName = path.basename(fileName, '.json');
105+
try {
106+
fs.writeFileSync(abiOnlyPath, abiOnlyContent);
107+
console.log(
108+
`ABI-only file generated for ${fileName}: ${abiOnlyPath}`,
69109
);
110+
} catch (err) {
111+
console.error(`Error writing ABI-only file for ${fileName}:`, err);
112+
return null;
113+
}
70114

71-
fs.writeFile(outputFilePath, tsContent, (err) => {
72-
if (err) {
73-
console.error(`Error writing TypeScript file for ${fileName}:`, err);
74-
} else {
75-
console.log(
76-
`TypeScript file generated for ${fileName}: ${outputFilePath}`,
77-
);
78-
addExportToIndexFile(path.basename(fileName, '.json'));
79-
}
80-
});
81-
} catch (parseError) {
82-
console.error(`Error parsing JSON from ${fileName}:`, parseError);
115+
try {
116+
fs.writeFileSync(bytecodeOnlyPath, bytecodeOnlyContent);
117+
console.log(
118+
`Bytecode-only file generated for ${fileName}: ${bytecodeOnlyPath}`,
119+
);
120+
} catch (err) {
121+
console.error(`Error writing bytecode-only file for ${fileName}:`, err);
122+
return null;
83123
}
84-
});
124+
return exportName;
125+
} catch (parseError) {
126+
console.error(`Error parsing JSON from ${fileName}:`, parseError);
127+
return null;
128+
}
85129
}
86130

87131
// Start processing from the input directory
88-
processDirectory(inputDir);
132+
const exportNames = processDirectory(inputDir);
133+
writeIndexFiles(exportNames);

packages/delegation-abis/scripts/generate.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ cd ../..
1818
# Synchronize files
1919
cd packages/delegation-abis
2020

21-
rsync -r ../../abisTemp1/ src/raw/
22-
rsync -r --ignore-existing ../../abisTemp2/ src/raw/
23-
rsync -r --ignore-existing ../../abisTemp3/ src/raw/
21+
rsync -r ../../abisTemp1/ src/artifacts/
22+
rsync -r --ignore-existing ../../abisTemp2/ src/artifacts/
23+
rsync -r --ignore-existing ../../abisTemp3/ src/artifacts/
2424
rm -r ../../abisTemp1
2525
rm -r ../../abisTemp2
2626
rm -r ../../abisTemp3

packages/delegation-abis/src/formatted/Address.ts renamed to packages/delegation-abis/src/abis/Address.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,3 @@ export const abi = [
2727
"inputs": []
2828
}
2929
] as const;
30-
31-
32-
export const bytecode = "0x60566037600b82828239805160001a607314602a57634e487b7160e01b600052600060045260246000fd5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea26469706673582212204a050c4fa34153d3fae9d282b8c08f593842bb7e1d0caa440348574559167b6a64736f6c63430008170033" as const;

0 commit comments

Comments
 (0)