diff --git a/package.json b/package.json index acb129b..7cca55d 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,10 @@ "license": "MIT", "version": "0.0.5", "description": "Edgelord Solidity snippets.", + "scripts": { + "powerup": "node scripts/powerup.js", + "rebrand": "node scripts/rebrand.js" + }, "files": [ "src/**/*.sol" ], diff --git a/scripts/config.js b/scripts/config.js new file mode 100644 index 0000000..9e74d89 --- /dev/null +++ b/scripts/config.js @@ -0,0 +1,11 @@ +/*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ +/* CONFIGS */ +/*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + +const LATEST_SOLIDITY_VERSION = "0.8.24"; +const SRC_DIR = "src"; + +module.exports = { + LATEST_SOLIDITY_VERSION, + SRC_DIR, +}; diff --git a/scripts/io.js b/scripts/io.js new file mode 100644 index 0000000..fe71541 --- /dev/null +++ b/scripts/io.js @@ -0,0 +1,109 @@ +const fs = require("fs"); +const path = require("path"); +const { SRC_DIR } = require("./config"); +/*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ +/* I/O Helpers */ +/*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + +/** + * Recursively retrieves a list of Solidity files within a given directory. + * + * @param {string} directory - The root directory to search for Solidity files. Defaults to SRC_DIR. + * @param {string[]} fileList - An optional array to accumulate file paths (used for recursion). + * @returns {string[]} An array of Solidity file paths. + */ +function getSolidityFiles(directory = SRC_DIR, fileList = []) { + const files = fs.readdirSync(directory); + + files.forEach((file) => { + const filePath = path.join(directory, file); + + if (fs.statSync(filePath).isDirectory()) { + getSolidityFiles(filePath, fileList); + } else if (file.endsWith(".sol")) { + fileList.push(filePath); + } + }); + return fileList; +} + +/** + * Shows the updates to be applied as a table. + * + * @param {Update[]} updates - An array of update objects containing scope, file paths, pattern, and patch. + */ +async function showUpdates(updates) { + console.log(`${updates.length} update(s) can be automatically applied:`); + + // Friendly table heading. + updates = updates.map((update) => ({ + "Update type": update.scope, + File: update.file, + "Current value": truncate(update.from), + "Proposed change": truncate(update.to), + })); + + // Increasing index so it starts from 1 instead of 0. + updates = updates.reduce((acc, u, i) => { + acc[i + 1] = u; + return acc; + }, {}); + + console.table(updates); +} + +/** + * Prompts the user for confirmation with a yes/no question. + * + * @param {string} prompt - The question to ask the user. + * @returns {Promise} A Promise that resolves to true if the user confirms, false otherwise. + */ +async function getUserConfirmation(prompt) { + const rl = require("readline").createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const answer = await new Promise((resolve) => { + rl.question(`${prompt} (y/n): `, resolve); + }); + rl.close(); + return answer.toLowerCase() === "y"; +} + +/** + * Applies updates to specified files. + * + * @param {Update[]} updates - An array of update objects containing scope, file paths, pattern, and patch. + */ +function applyUpdates(updates) { + updates.forEach((update) => { + let content = fs.readFileSync(update.file, "utf-8"); + // The update query may contain reserved regex characters + // that needs to be escaped to be considered literally. + let inputString = update.from.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + content = content.replace(new RegExp(inputString, "g"), update.to); + fs.writeFileSync(update.file, content, "utf-8"); + }); + console.log( + `🎉 ${updates.length} update(s) applied.\n👀 Please verify before committing to git.` + ); +} +/** + * Adds ellipis to text beyond a defined length. + * + * @param {string} Text the text to truncate. + * @returns {string} The truncated text. + */ +function truncate(text) { + let length = 30; + if (text.length <= length) return text; + return text.substr(0, 30) + "\u2026"; +} + +module.exports = { + getSolidityFiles, + showUpdates, + getUserConfirmation, + applyUpdates, +}; diff --git a/scripts/powerup.js b/scripts/powerup.js new file mode 100644 index 0000000..5bf3b5a --- /dev/null +++ b/scripts/powerup.js @@ -0,0 +1,53 @@ +const fs = require("fs"); +const { LATEST_SOLIDITY_VERSION } = require("./config"); +const { + getSolidityFiles, + showUpdates, + getUserConfirmation, + applyUpdates, +} = require("./io"); + +/** + * Powerup: Updates solidity version to `LATEST_SOLIDITY_VERSION`. + */ +async function powerup() { + const updates = []; + + for (let filePath of getSolidityFiles()) { + const oldVersion = getSolidityVersion(filePath); + if (!oldVersion) { + console.log(`Could not determine Solidity version in ${filePath}`); + continue; + } + + if (oldVersion != LATEST_SOLIDITY_VERSION) { + updates.push({ + scope: "Solidity version", + file: filePath, + from: oldVersion, + to: LATEST_SOLIDITY_VERSION, + }); + } + } + + if (updates.length > 0) { + showUpdates(updates); + const confirmation = await getUserConfirmation( + "Do you want to upgrade these file(s) to the latest Solidity version?" + ); + if (confirmation) applyUpdates(updates); + } else { + console.log( + `🎉 All files are already on the latest Solidity version (${LATEST_SOLIDITY_VERSION})!` + ); + } +} + +function getSolidityVersion(filePath) { + const content = fs.readFileSync(filePath, "utf-8"); + const versionMatch = content.match(/pragma solidity (\^)?(\d+\.\d+\.\d+);/); + + return versionMatch ? versionMatch[2] : null; +} + +powerup(); diff --git a/scripts/rebrand.js b/scripts/rebrand.js new file mode 100644 index 0000000..2d2a931 --- /dev/null +++ b/scripts/rebrand.js @@ -0,0 +1,63 @@ +const fs = require("fs"); +const { + getSolidityFiles, + showUpdates, + getUserConfirmation, + applyUpdates, +} = require("./io"); + +/** + * Rebrand: Migrate branding from solady to soledge. + * Currently, only comments are being rebranded. + */ +async function rebrand() { + const updates = []; + const COMMENT_MAPPING = [ + { + from: "/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/", + to: "/*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/", + }, + { + from: "/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/", + to: "/*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/", + }, + ]; + + for (let filePath of getSolidityFiles()) { + for (let patch of COMMENT_MAPPING) { + if (fileContainsPattern(filePath, patch.from)) { + updates.push({ + scope: "soledge branding", + file: filePath, + from: patch.from, + to: patch.to, + }); + } + } + } + + if (updates.length > 0) { + showUpdates(updates); + const confirmation = await getUserConfirmation( + "Confirm applying the proposed branding updates?" + ); + if (confirmation) applyUpdates(updates); + } else { + console.log(`🎉 All files already adheres to soledge branding guidelines.`); + } +} + +function fileContainsPattern(filePath, pattern) { + const content = fs.readFileSync(filePath, "utf-8"); + + // Escape regex special characters from pattern + pattern = pattern.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + + // Use the escaped pattern in the regex match + const regex = new RegExp(pattern); + + // Check if the content matches the pattern + return regex.test(content); +} + +rebrand();