Feat/husky lint (#431)

* add poc husky lint script

* test commit change

* undo test change

* test change

* undo test change

* test

* test

* test

* test

* do

* test

* test

* test

* test

* test

* test

* test

* test

* test

* test

* refactor to pre-commit to NodeJS script

* add logs for demo

* small refactor of pre-commit.js

* add shebang to pre-commit

* add other folders to husky script

* added postman to pre-commit

* remove test md change

* add comments for pre-commit.js

* adjust lint:fix scripts

* Update .husky/pre-commit.js

Co-authored-by: The Dark Jester <thedarkjester@users.noreply.github.com>
Signed-off-by: kyzooghost <73516204+kyzooghost@users.noreply.github.com>

---------

Signed-off-by: kyzooghost <73516204+kyzooghost@users.noreply.github.com>
Co-authored-by: The Dark Jester <thedarkjester@users.noreply.github.com>
This commit is contained in:
kyzooghost
2024-12-20 23:34:59 +11:00
committed by GitHub
parent 4a7b76c90d
commit a5119c43b0
6 changed files with 246 additions and 3 deletions

10
.husky/pre-commit Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
# Run `npx husky` in root directory to initialize this pre-commit hook for local 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

217
.husky/pre-commit.js Normal file
View File

@@ -0,0 +1,217 @@
/**
* Runs as git pre-commit hook
* Filters the list of changed files on 'git commit'
* If *.ts files in specified projects are detected, runs the 'lint:ts:fix' package.json script for that project
* E.g. if a *.ts file is changed in /sdk, then this script will run 'pnpm run lint:ts:fix' in the /sdk project
*/
const fs = require('fs');
const { execSync } = require('child_process');
/**
* ENUMS
*/
// File extensions to filter for
const FILE_EXTENSION = {
TYPESCRIPT: "TYPESCRIPT",
SOLIDITY: "SOLIDITY",
}
// Projects to filter for
const FOLDER = {
BRIDGEUI: "BRIDGEUI",
CONTRACTS: "CONTRACTS",
E2E: "E2E",
OPERATIONS: "OPERATIONS",
POSTMAN: "POSTMAN",
SDK: "SDK",
}
// Project runtimes
const RUNTIME = {
NODEJS: "NODEJS"
}
/**
* MAPPINGS
*/
// File extension => regex
const FILE_EXTENSION_FILTERS = {
[FILE_EXTENSION.TYPESCRIPT]: "\.ts$",
[FILE_EXTENSION.SOLIDITY]: "\.sol$",
};
// File extension => script in package.json to run
const FILE_EXTENSION_LINTING_COMMAND = {
[FILE_EXTENSION.TYPESCRIPT]: "pnpm run lint:ts:fix",
[FILE_EXTENSION.SOLIDITY]: "pnpm run lint:sol:fix",
};
// Project => Path in monorepo
const FOLDER_PATH = {
[FOLDER.BRIDGEUI]: "bridge-ui/",
[FOLDER.CONTRACTS]: "contracts/",
[FOLDER.E2E]: "e2e/",
[FOLDER.OPERATIONS]: "operations/",
[FOLDER.POSTMAN]: "postman/",
[FOLDER.SDK]: "sdk/",
};
// Project => List of changed files
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(),
};
// Project => Runtime
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
*/
/**
* Gets a list of changed files in the git commit
* @returns {string[]}
*/
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)
}
}
/**
* Partitions list of changed files from getChangedFileList() by project
* Stores results in FOLDER_CHANGED_FILES
* @param {string[]}
*/
function partitionChangedFileList(_changedFileList) {
for (const file of _changedFileList) {
for (const path in FOLDER) {
if (file.match(new RegExp(`^${FOLDER_PATH[path]}`))) {
FOLDER_CHANGED_FILES[path].push(file);
}
}
}
}
/**
* Checks if runtime dependencies are installed for a project
* @param {FOLDER}
* @returns {boolean}
*/
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
}
}
/**
* Resolve list of changed file extensions for a project
* @param {FOLDER}
* @returns {FILE_EXTENSION[]}
*/
function getChangedFileExtensions(_folder) {
// Use sets to implement early exit from loop, once we have matched all configured file extensions
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);
}
/**
* Execute linting command
* @param {FOLDER, FILE_EXTENSION[]}
*/
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 stream 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);
}
}
}
/**
* Redo `git add` for files updated during executeLinting(), so that they are not left out of the commit
* The difference between 'git add .' and 'git update-index --again', is that the latter will not include untracked files
*/
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);
}
}

View File

@@ -8,7 +8,8 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"lint:fix": "pnpm run lint:ts: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",

View File

@@ -9,7 +9,8 @@
"prettier": "prettier -c '**/*.{js,ts}'",
"prettier:fix": "prettier -w '**/*.{js,ts}'",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"lint:fix": "pnpm run lint: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",

View File

@@ -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",

12
pnpm-lock.yaml generated
View File

@@ -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==}
@@ -17287,6 +17297,8 @@ snapshots:
human-signals@5.0.0: {}
husky@9.1.7: {}
i18next-browser-languagedetector@7.1.0:
dependencies:
'@babel/runtime': 7.25.7