diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..f9f0dc8c6 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,10 @@ +#!/bin/bash + +# For this to run on pre-commit hook, should run `npx husky` in root after `git clone` repo onto dev machine + +# Execute NodeJS script because +# i.) Husky requires NodeJS -> fair assumption that machine will have NodeJS +# ii.) Cleaner syntax and abstractions than shell scripting +node .husky/pre-commit.js + +exit 0 \ No newline at end of file diff --git a/.husky/pre-commit.js b/.husky/pre-commit.js new file mode 100644 index 000000000..91e1ebbae --- /dev/null +++ b/.husky/pre-commit.js @@ -0,0 +1,169 @@ +const fs = require('fs'); +const { execSync } = require('child_process'); + +// ENUMS + +const FILE_EXTENSION = { + TYPESCRIPT: "TYPESCRIPT", + SOLIDITY: "SOLIDITY", +} + +const FOLDER = { + BRIDGEUI: "BRIDGEUI", + CONTRACTS: "CONTRACTS", + E2E: "E2E", + OPERATIONS: "OPERATIONS", + POSTMAN: "POSTMAN", + SDK: "SDK", +} + +const RUNTIME = { + NODEJS: "NODEJS" +} + +// MAPS + +const FILE_EXTENSION_FILTERS = { + [FILE_EXTENSION.TYPESCRIPT]: "\.ts$", + [FILE_EXTENSION.SOLIDITY]: "\.sol$", +}; + +const FILE_EXTENSION_LINTING_COMMAND = { + [FILE_EXTENSION.TYPESCRIPT]: "pnpm run lint:ts:fix", + [FILE_EXTENSION.SOLIDITY]: "pnpm run lint:sol", +}; + +const FOLDER_PATH = { + + [FOLDER.BRIDGEUI]: "bridge-ui/", + [FOLDER.CONTRACTS]: "contracts/", + [FOLDER.E2E]: "e2e/", + [FOLDER.OPERATIONS]: "operations/", + [FOLDER.POSTMAN]: "postman/", + [FOLDER.SDK]: "sdk/", +}; + +const FOLDER_CHANGED_FILES = { + [FOLDER.BRIDGEUI]: new Array(), + [FOLDER.CONTRACTS]: new Array(), + [FOLDER.E2E]: new Array(), + [FOLDER.OPERATIONS]: new Array(), + [FOLDER.POSTMAN]: new Array(), + [FOLDER.SDK]: new Array(), +}; + +const FOLDER_RUNTIME = { + [FOLDER.BRIDGEUI]: RUNTIME.NODEJS, + [FOLDER.CONTRACTS]: RUNTIME.NODEJS, + [FOLDER.E2E]: RUNTIME.NODEJS, + [FOLDER.OPERATIONS]: RUNTIME.NODEJS, + [FOLDER.POSTMAN]: RUNTIME.NODEJS, + [FOLDER.SDK]: RUNTIME.NODEJS, +}; + +// MAIN FUNCTION + +main(); + +function main() { + const changedFileList = getChangedFileList(); + partitionChangedFileList(changedFileList); + + for (const folder in FOLDER) { + if (!isDependenciesInstalled(folder)) { + console.error(`Dependencies not installed in ${FOLDER_PATH[folder]}, exiting...`) + process.exit(1); + } + const changedFileExtensions = getChangedFileExtensions(folder); + executeLinting(folder, changedFileExtensions); + } + + updateGitIndex(); +} + +// HELPER FUNCTIONS + +function getChangedFileList() { + try { + const cmd = 'git diff --name-only HEAD' + const stdout = execSync(cmd, { encoding: 'utf8' }); + return stdout.split('\n').filter(file => file.trim() !== ''); + } catch (error) { + console.error($`Error running ${cmd}:`, error.message); + process.exit(1) + } +} + +function partitionChangedFileList(_changedFileList) { + // Populate lists of filter matches + for (const file of _changedFileList) { + for (const path in FOLDER) { + if (file.match(new RegExp(`^${FOLDER_PATH[path]}`))) { + FOLDER_CHANGED_FILES[path].push(file); + } + } + } +} + +function isDependenciesInstalled(_folder) { + const runtime = FOLDER_RUNTIME[_folder]; + const path = FOLDER_PATH[_folder]; + + switch(runtime) { + case RUNTIME.NODEJS: + const dependencyFolder = `${path}node_modules` + return fs.existsSync(dependencyFolder) + default: + console.error(`${runtime} runtime not supported.`); + return false + } +} + +function getChangedFileExtensions(_folder) { + // Use sets to implement early exit from iteration of all changed files, once we have matched all file extensions of interest. + const remainingFileExtensionsSet = new Set(Object.values(FILE_EXTENSION)); + const foundFileExtensionsSet = new Set(); + + for (const file of FOLDER_CHANGED_FILES[_folder]) { + for (const fileExtension of remainingFileExtensionsSet) { + if (file.match(new RegExp(FILE_EXTENSION_FILTERS[fileExtension]))) { + foundFileExtensionsSet.add(fileExtension); + remainingFileExtensionsSet.delete(fileExtension); + } + } + + // No more remaining file extensions to look for + if (remainingFileExtensionsSet.size == 0) break; + } + + return Array.from(foundFileExtensionsSet); +} + +function executeLinting(_folder, _changedFileExtensions) { + for (const fileExtension of _changedFileExtensions) { + const path = FOLDER_PATH[_folder]; + const cmd = FILE_EXTENSION_LINTING_COMMAND[fileExtension]; + console.log(`${fileExtension} change found in ${path}, linting...`); + try { + // Execute command synchronously and route output directly to the current stdout + execSync(` + cd ${path}; + ${cmd}; + `, { stdio: 'inherit' }); + } catch (error) { + console.error(`Error:`, error.message); + console.error(`Exiting...`); + process.exit(1); + } + } +} + +function updateGitIndex() { + try { + const cmd = 'git update-index --again' + execSync(cmd, { stdio: 'inherit' }); + } catch (error) { + console.error($`Error running ${cmd}:`, error.message); + process.exit(1); + } +} \ No newline at end of file diff --git a/bridge-ui/package.json b/bridge-ui/package.json index f92a83960..b3d5ede76 100644 --- a/bridge-ui/package.json +++ b/bridge-ui/package.json @@ -9,6 +9,7 @@ "start": "next start", "lint": "next lint", "lint:fix": "next lint --fix", + "lint:ts:fix": "next lint --fix", "clean": "rimraf node_modules .next .next-env.d.ts", "install:playwright": "playwright install --with-deps", "build:cache": "synpress", diff --git a/operations/package.json b/operations/package.json index a3d436369..374780f27 100644 --- a/operations/package.json +++ b/operations/package.json @@ -10,6 +10,7 @@ "prettier:fix": "prettier -w '**/*.{js,ts}'", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", + "lint:ts:fix": "eslint . --ext .ts --fix", "test": "node --experimental-vm-modules node_modules/jest/bin/jest --bail --detectOpenHandles --forceExit", "clean": "rimraf node_modules dist coverage", "postpack": "shx rm -f oclif.manifest.json", diff --git a/package.json b/package.json index 77b4852bf..18f8e6971 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "lint:fix": "pnpm run -r --if-present lint:fix", "clean": "pnpm run -r --if-present clean && rm -rf node_modules", "test": "pnpm run -r --if-present test", - "build": "pnpm run -r --if-present build" + "build": "pnpm run -r --if-present build", + "prepare": "husky" }, "devDependencies": { "@types/node": "20.12.7", @@ -23,6 +24,7 @@ "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-prettier": "5.1.3", + "husky": "9.1.7", "prettier": "3.2.5", "rimraf": "5.0.5", "ts-node": "10.9.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d696aa53..6e135216b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: eslint-plugin-prettier: specifier: 5.1.3 version: 5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.2.5) + husky: + specifier: 9.1.7 + version: 9.1.7 prettier: specifier: 3.2.5 version: 3.2.5 @@ -260,6 +263,8 @@ importers: specifier: 17.7.2 version: 17.7.2 + contracts/lib/forge-std: {} + e2e: devDependencies: '@jest/globals': @@ -5861,6 +5866,11 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + i18next-browser-languagedetector@7.1.0: resolution: {integrity: sha512-cr2k7u1XJJ4HTOjM9GyOMtbOA47RtUoWRAtt52z43r3AoMs2StYKyjS3URPhzHaf+mn10hY9dZWamga5WPQjhA==} @@ -17303,6 +17313,8 @@ snapshots: human-signals@5.0.0: {} + husky@9.1.7: {} + i18next-browser-languagedetector@7.1.0: dependencies: '@babel/runtime': 7.25.7