-
-
Notifications
You must be signed in to change notification settings - Fork 24
Improve @metamask/delegation-abis tree-shakability #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
596b955
Split ABIs and bytecode, enabling tree-shaking per contract; does _no…
jeffsmale90 97f3c75
Update generated files
jeffsmale90 504debb
Update deploySmartAccountsEnvironment to use new abi imports
jeffsmale90 185fea0
Update all references to @metamask/delegation-abis that now import on…
jeffsmale90 85e0933
Remove redundant NarrowAbiToFunction utility type
jeffsmale90 eff4692
Update e2e test suite to use new interface
jeffsmale90 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| // From https://github.com/aymericzip/esbuild-fix-imports-plugin/blob/main/src/fixExtensionsPlugin.ts, included here to avoid adding a dependency | ||
| import type { Plugin } from 'esbuild'; | ||
| import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'; | ||
| import { join, extname } from 'node:path'; | ||
|
|
||
| const CJS_RELATIVE_IMPORT_EXP = /require\s*\(\s*["'](\..+?)["']\s*\)(;?)/g; | ||
| const ESM_RELATIVE_IMPORT_EXP = /from\s*(["'])(\.[^"']+)\1([^;]*;?)/g; | ||
| const hasJSExtensionRegex = /\.(?:js)$/i; | ||
| const hasNonJSExtensionRegex = | ||
| /\.(?: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; | ||
|
|
||
| function modifyEsmImports(contents: string, outExt: string) { | ||
| return contents.replace(ESM_RELATIVE_IMPORT_EXP, (_m, quote, importPath, rest = '') => { | ||
| if (importPath.endsWith('.') || importPath.endsWith('/')) { | ||
| return `from ${quote}${importPath}/index${outExt}${quote}${rest}`; | ||
| } | ||
| if (importPath.endsWith(outExt)) { | ||
| return `from ${quote}${importPath}${quote}${rest}`; | ||
| } | ||
| if (hasJSExtensionRegex.test(importPath) && outExt !== '.js') { | ||
| return `from ${quote}${importPath.replace(hasJSExtensionRegex, outExt)}${quote}${rest}`; | ||
| } | ||
| if (hasNonJSExtensionRegex.test(importPath)) { | ||
| return `from ${quote}${importPath}${quote}${rest}`; | ||
| } | ||
| return `from ${quote}${importPath}${outExt}${quote}${rest}`; | ||
| }); | ||
| } | ||
|
|
||
| function modifyCjsImports(contents: string, outExt: string) { | ||
| return contents.replace(CJS_RELATIVE_IMPORT_EXP, (_m, importPath, maybeSemi = '') => { | ||
| if (importPath.endsWith('.') || importPath.endsWith('/')) { | ||
| return `require('${importPath}/index${outExt}')${maybeSemi}`; | ||
| } | ||
| if (hasJSExtensionRegex.test(importPath) && outExt !== '.js') { | ||
| return `require('${importPath.replace(hasJSExtensionRegex, outExt)}')${maybeSemi}`; | ||
| } | ||
| if (importPath.endsWith(outExt) || hasNonJSExtensionRegex.test(importPath)) { | ||
| return `require('${importPath}')${maybeSemi}`; | ||
| } | ||
| return `require('${importPath}${outExt}')${maybeSemi}`; | ||
| }); | ||
| } | ||
|
|
||
| function modifyRelativeImports(contents: string, isEsm: boolean, outExt: string) { | ||
| return isEsm ? modifyEsmImports(contents, outExt) : modifyCjsImports(contents, outExt); | ||
| } | ||
|
|
||
| // Fallback if esbuild doesn't provide outputFiles (write: true) | ||
| function processOutdirFiles(outdir: string, isEsm: boolean, outExt: string) { | ||
| const stack = [outdir]; | ||
| while (stack.length) { | ||
| const dir = stack.pop()!; | ||
| for (const entry of readdirSync(dir)) { | ||
| const p = join(dir, entry); | ||
| if (statSync(p).isDirectory()) { | ||
| stack.push(p); | ||
| continue; | ||
| } | ||
| const ext = extname(p); | ||
| if (ext !== outExt) { | ||
| continue; | ||
| } | ||
| if (ext === '.mjs' || ext === '.cjs' || ext === '.js') { | ||
| const original = readFileSync(p, 'utf8'); | ||
| const updated = modifyRelativeImports(original, isEsm || ext === '.mjs', outExt); | ||
| if (updated !== original) { | ||
| writeFileSync(p, updated); | ||
jeffsmale90 marked this conversation as resolved.
Dismissed
Show dismissed
Hide dismissed
|
||
| } | ||
| } | ||
jeffsmale90 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| export const fixExtensionsPlugin = (): Plugin => ({ | ||
| name: 'fixExtensionsPlugin', | ||
| setup(build) { | ||
| const isEsm = build.initialOptions.format === 'esm'; | ||
| const outExt = build.initialOptions.outExtension?.['.js'] ?? (isEsm ? '.mjs' : '.cjs'); | ||
| const outdir = build.initialOptions.outdir; | ||
|
|
||
| build.onEnd((result) => { | ||
| if (result.errors.length > 0) return; | ||
|
|
||
| if (result.outputFiles && result.outputFiles.length > 0) { | ||
| for (const ofile of result.outputFiles) { | ||
| if (!ofile.path.endsWith(outExt)) continue; | ||
| const next = modifyRelativeImports(ofile.text, isEsm, outExt); | ||
| ofile.contents = Buffer.from(next); | ||
| } | ||
| } else if (outdir) { | ||
| processOutdirFiles(outdir, isEsm, outExt); | ||
| } | ||
| }); | ||
| }, | ||
| }); | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,88 +1,133 @@ | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
|
|
||
| // Directory containing the JSON ABI files | ||
| const inputDir = './src/raw'; | ||
| // Output directory for the TypeScript files | ||
| const outputDir = './src/formatted'; | ||
| const inputDir = './src/artifacts'; | ||
| const abiDir = './src/abis'; | ||
| const bytecodeModulesDir = './src/bytecode'; | ||
| const INDEX_FILE = './src/index.ts'; | ||
| const BYTECODE_FILE = './src/bytecode.ts'; | ||
|
|
||
| // Ensure the output directory exists | ||
| if (!fs.existsSync(outputDir)) { | ||
| fs.mkdirSync(outputDir, { recursive: true }); | ||
| if (!fs.existsSync(abiDir)) { | ||
| fs.mkdirSync(abiDir, { recursive: true }); | ||
| } | ||
| if (!fs.existsSync(bytecodeModulesDir)) { | ||
| fs.mkdirSync(bytecodeModulesDir, { recursive: true }); | ||
| } | ||
|
|
||
| // Export ts in an index file | ||
| function addExportToIndexFile(fileName) { | ||
| const indexStream = fs.createWriteStream('./src/index.ts', { | ||
| flags: 'a', | ||
| }); | ||
| // Initialize index files (truncate) | ||
| fs.writeFileSync(INDEX_FILE, ''); | ||
| fs.writeFileSync(BYTECODE_FILE, ''); | ||
|
|
||
| indexStream.write(`export * as ${fileName} from './formatted/${fileName}'\n`); | ||
| // Write exports to index and bytecode files in sorted order | ||
| function writeIndexFiles(exportNames) { | ||
| const sortedNames = exportNames | ||
| .slice() | ||
| .sort((a, b) => a.localeCompare(b)); | ||
| const indexContents = sortedNames | ||
| .map((fileName) => `export { abi as ${fileName} } from './abis/${fileName}';`) | ||
| .join('\n'); | ||
| const bytecodeContents = sortedNames | ||
| .map( | ||
| (fileName) => `export { bytecode as ${fileName} } from './bytecode/${fileName}';`, | ||
| ) | ||
| .join('\n'); | ||
|
|
||
| indexStream.end(); | ||
| fs.writeFileSync(INDEX_FILE, `${indexContents}\n`); | ||
| fs.writeFileSync(BYTECODE_FILE, `${bytecodeContents}\n`); | ||
| } | ||
|
|
||
| // Recursive function to process files and directories | ||
| function processDirectory(directory) { | ||
| fs.readdir(directory, { withFileTypes: true }, (err, entries) => { | ||
| if (err) { | ||
| console.error('Error reading directory:', err); | ||
| return; | ||
| } | ||
| const exportNames = []; | ||
| let entries; | ||
| try { | ||
| entries = fs.readdirSync(directory, { withFileTypes: true }); | ||
| } catch (err) { | ||
| console.error('Error reading directory:', err); | ||
| return exportNames; | ||
| } | ||
|
|
||
| const ignoreList = ['utils', 'build-info']; | ||
| entries.forEach((entry) => { | ||
| const ignoreList = ['utils', 'build-info']; | ||
| entries | ||
| .slice() | ||
| .sort((a, b) => a.name.localeCompare(b.name)) | ||
| .forEach((entry) => { | ||
| if (!ignoreList.includes(path.basename(directory))) { | ||
| const fullPath = path.join(directory, entry.name); | ||
| if (entry.isDirectory()) { | ||
| // Recurse into subdirectories | ||
| processDirectory(fullPath); | ||
| exportNames.push(...processDirectory(fullPath)); | ||
| } else if (path.extname(entry.name) === '.json') { | ||
| // Process JSON files | ||
| processFile(fullPath, entry.name); | ||
| const exportName = processFile(fullPath, entry.name); | ||
| if (exportName) { | ||
| exportNames.push(exportName); | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| return exportNames; | ||
| } | ||
|
|
||
| // Function to process each JSON file | ||
| function processFile(filePath, fileName) { | ||
| fs.readFile(filePath, 'utf8', (err, data) => { | ||
| if (err) { | ||
| console.error(`Error reading file ${fileName}:`, err); | ||
| return; | ||
| } | ||
| let data; | ||
| try { | ||
| data = fs.readFileSync(filePath, 'utf8'); | ||
| } catch (err) { | ||
| console.error(`Error reading file ${fileName}:`, err); | ||
| return null; | ||
| } | ||
|
|
||
| try { | ||
| const abi = JSON.parse(data); | ||
| const tsContent = `export const abi = ${JSON.stringify( | ||
| abi.abi, | ||
| null, | ||
| 2, | ||
| )} as const;\n | ||
| try { | ||
| const parsed = JSON.parse(data); | ||
| const abi = parsed.abi; | ||
| const bytecode = parsed.bytecode?.object ?? ''; | ||
| const abiOnlyContent = `export const abi = ${JSON.stringify( | ||
| abi, | ||
| null, | ||
| 2, | ||
| )} as const;\n`; | ||
| const bytecodeOnlyContent = `export const bytecode = '${bytecode}' as const;`; | ||
| const abiOnlyPath = path.join( | ||
| abiDir, | ||
| `${path.basename(fileName, '.json')}.ts`, | ||
| ); | ||
| const bytecodeOnlyPath = path.join( | ||
| bytecodeModulesDir, | ||
| `${path.basename(fileName, '.json')}.ts`, | ||
| ); | ||
|
|
||
| export const bytecode = \"${abi.bytecode.object}\" as const;`; | ||
| const outputFilePath = path.join( | ||
| outputDir, | ||
| `${path.basename(fileName, '.json')}.ts`, | ||
| // Write abi-only and bytecode-only modules | ||
| const exportName = path.basename(fileName, '.json'); | ||
| try { | ||
| fs.writeFileSync(abiOnlyPath, abiOnlyContent); | ||
| console.log( | ||
| `ABI-only file generated for ${fileName}: ${abiOnlyPath}`, | ||
| ); | ||
| } catch (err) { | ||
| console.error(`Error writing ABI-only file for ${fileName}:`, err); | ||
| return null; | ||
| } | ||
|
|
||
| fs.writeFile(outputFilePath, tsContent, (err) => { | ||
| if (err) { | ||
| console.error(`Error writing TypeScript file for ${fileName}:`, err); | ||
| } else { | ||
| console.log( | ||
| `TypeScript file generated for ${fileName}: ${outputFilePath}`, | ||
| ); | ||
| addExportToIndexFile(path.basename(fileName, '.json')); | ||
| } | ||
| }); | ||
| } catch (parseError) { | ||
| console.error(`Error parsing JSON from ${fileName}:`, parseError); | ||
| try { | ||
| fs.writeFileSync(bytecodeOnlyPath, bytecodeOnlyContent); | ||
| console.log( | ||
| `Bytecode-only file generated for ${fileName}: ${bytecodeOnlyPath}`, | ||
| ); | ||
| } catch (err) { | ||
| console.error(`Error writing bytecode-only file for ${fileName}:`, err); | ||
| return null; | ||
| } | ||
| }); | ||
| return exportName; | ||
| } catch (parseError) { | ||
| console.error(`Error parsing JSON from ${fileName}:`, parseError); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| // Start processing from the input directory | ||
| processDirectory(inputDir); | ||
| const exportNames = processDirectory(inputDir); | ||
| writeIndexFiles(exportNames); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.