├── .husky ├── .gitignore └── pre-commit ├── .gitignore ├── src ├── bin.ts └── index.ts ├── .eslintrc.json ├── lib ├── bin.js └── index.js ├── tsconfig.json ├── package.json ├── LICENSE └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | npm run build 6 | git add lib 7 | -------------------------------------------------------------------------------- /src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { run } from './' 3 | 4 | const [, , arg] = process.argv 5 | 6 | run(arg === '--remove-v4-config') 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@typicode", 3 | "rules": { 4 | "@typescript-eslint/ban-ts-comment": "off", 5 | "@typescript-eslint/no-unsafe-call": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | const _1 = require("./"); 5 | const [, , arg] = process.argv; 6 | _1.run(arg === '--remove-v4-config'); 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node12/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./lib", 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "noImplicitReturns": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "removeComments": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "husky-4-to-8", 4 | "version": "1.0.0", 5 | "bin": "lib/bin.js", 6 | "files": [ 7 | "/lib" 8 | ], 9 | "dependencies": { 10 | "chalk": "^4.1.0", 11 | "cosmiconfig": "^7.0.0", 12 | "husky": "^8" 13 | }, 14 | "devDependencies": { 15 | "@tsconfig/node12": "^1.0.7", 16 | "@types/node": "^14.14.22", 17 | "@typicode/eslint-config": "^0.1.2", 18 | "del-cli": "^3.0.1", 19 | "type-fest": "^0.21.2", 20 | "typescript": "^4.1.3" 21 | }, 22 | "scripts": { 23 | "prebuild": "del-cli lib", 24 | "build": "tsc", 25 | "prepare": "husky install", 26 | "lint": "eslint src" 27 | }, 28 | "repository": "typicode/husky-4-to-8.git", 29 | "author": "Typicode ", 30 | "license": "MIT" 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 typicode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # husky-4-to-8 2 | 3 | > Easily migrate your husky 4 config to husky 8 4 | 5 | While it should cover most basic migrations, it's **recommended** to have a look at husky 8 [documentation](https://typicode.github.io/husky). 6 | 7 | If your `package.json` is not at the same level as `.git`, please update manually. 8 | 9 | ## Usage 10 | 11 | ### npm 12 | 13 | ```shell 14 | npm install husky@latest --save-dev \ 15 | && npx husky-init \ 16 | && npm exec -- github:typicode/husky-4-to-8 --remove-v4-config 17 | ``` 18 | 19 | ### yarn 20 | 21 | Yarn 1 22 | 23 | ```shell 24 | yarn add husky@latest --dev \ 25 | && npx husky-init \ 26 | && npm exec -- github:typicode/husky-4-to-8 --remove-v4-config 27 | ``` 28 | 29 | Yarn 2+ 30 | 31 | ```shell 32 | yarn add husky@latest --dev \ 33 | && yarn dlx husky-init --yarn2 \ 34 | && npm exec -- github:typicode/husky-4-to-8 --remove-v4-config 35 | ``` 36 | 37 | ### pnpm 38 | 39 | ```shell 40 | pnpm install husky@8 --save-dev \ 41 | && pnpx husky-init \ 42 | && pnpx -- github:typicode/husky-4-to-8 --remove-v4-config 43 | ``` 44 | 45 | ## What each command does 46 | 47 | `husky init` sets up Git hooks and updates your `package.json` scripts (you may want to commit your changes to `package.json` before running `husky init`). 48 | 49 | `husky-4-to-8` creates hooks based on your husky v4 config. If `--remove-v4-config` is passed, previous config will be deleted (recommended). 50 | 51 | ## Revert 52 | 53 | If there's an error during the process, you can clean things up by running: 54 | 55 | ```sh 56 | rm -rf .husky && git config --unset core.hooksPath 57 | ``` 58 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { cosmiconfigSync } from 'cosmiconfig' 3 | import fs from 'fs' 4 | import { set } from 'husky' 5 | 6 | function searchResult(): { 7 | hooks: { [key: string]: string } 8 | filepath: string | undefined 9 | } { 10 | const explorer = cosmiconfigSync('husky') 11 | 12 | const result = explorer.search() 13 | if (result === null) { 14 | throw new Error('no husky 4 config found') 15 | } 16 | 17 | interface Config { 18 | hooks?: { [key: string]: string } 19 | } 20 | const config = result?.config as Config 21 | 22 | return { 23 | hooks: config?.hooks || {}, 24 | filepath: result?.filepath, 25 | } 26 | } 27 | 28 | function showManualUpdateMessage(hooks: { [key: string]: string }) { 29 | const names: string[] = [] 30 | 31 | // Simple heuristic to check if hook needs to be manually updated 32 | const packageManagers = ['npm', 'npx', 'yarn', 'pnpm', 'pnpx'] 33 | const otherCriterias = ['HUSKY_GIT_PARAMS', '&&', '||'] 34 | if (hooks) { 35 | Object.entries(hooks).forEach(([name, script]) => { 36 | if ( 37 | !packageManagers.some((s) => script.startsWith(s)) || 38 | otherCriterias.some((s) => script.includes(s)) 39 | ) { 40 | names.push(name) 41 | } 42 | }) 43 | } 44 | 45 | // Show manual update message 46 | if (names.length > 0) { 47 | console.log(chalk` 48 | {red ⚠️ {bold ${names.join(', ')}} hook${ 49 | names.length > 1 ? 's' : '' 50 | } may need to be manually updated to be run via package manager.} 51 | 52 | {bold Examples:} 53 | jest → npx --no-install jest 54 | → yarn jest 55 | 56 | jest && eslint → npx --no-install jest && npx --no-install eslint 57 | → yarn jest && yarn eslint 58 | 59 | commitlint -E HUSKY_GIT_PARAMS → npx --no-install commitlint --edit $1 60 | → yarn commitlint --edit $1 61 | 62 | See {underline https://typicode.github.io/husky/#/?id=migrate-from-v4-to-v8} 63 | `) 64 | } 65 | } 66 | 67 | export function run(removeV4Config: boolean): void { 68 | const { hooks, filepath } = searchResult() 69 | 70 | Object.entries(hooks).forEach(([name, script]) => { 71 | const file = `.husky/${name}` 72 | set(file, script) 73 | }) 74 | 75 | if (removeV4Config && filepath) { 76 | if (filepath.endsWith('package.json')) { 77 | const str = fs.readFileSync('package.json', 'utf-8') 78 | const regex = /^[ ]+|\t+/m 79 | const indent = regex.exec(str)?.[0] 80 | const pkg = JSON.parse(str) // eslint-disable-line 81 | delete pkg.husky // eslint-disable-line 82 | fs.writeFileSync('package.json', `${JSON.stringify(pkg, null, indent)}\n`) 83 | console.log('husky - deleted husky field from package.json') 84 | } else { 85 | fs.unlinkSync(filepath) 86 | console.log(`husky - removed ${filepath}`) 87 | } 88 | } 89 | 90 | showManualUpdateMessage(hooks) 91 | } 92 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.run = void 0; 7 | const chalk_1 = __importDefault(require("chalk")); 8 | const cosmiconfig_1 = require("cosmiconfig"); 9 | const fs_1 = __importDefault(require("fs")); 10 | const husky_1 = require("husky"); 11 | function searchResult() { 12 | const explorer = cosmiconfig_1.cosmiconfigSync('husky'); 13 | const result = explorer.search(); 14 | if (result === null) { 15 | throw new Error('no husky 4 config found'); 16 | } 17 | const config = result === null || result === void 0 ? void 0 : result.config; 18 | return { 19 | hooks: (config === null || config === void 0 ? void 0 : config.hooks) || {}, 20 | filepath: result === null || result === void 0 ? void 0 : result.filepath, 21 | }; 22 | } 23 | function showManualUpdateMessage(hooks) { 24 | const names = []; 25 | const packageManagers = ['npm', 'npx', 'yarn', 'pnpm', 'pnpx']; 26 | const otherCriterias = ['HUSKY_GIT_PARAMS', '&&', '||']; 27 | if (hooks) { 28 | Object.entries(hooks).forEach(([name, script]) => { 29 | if (!packageManagers.some((s) => script.startsWith(s)) || 30 | otherCriterias.some((s) => script.includes(s))) { 31 | names.push(name); 32 | } 33 | }); 34 | } 35 | if (names.length > 0) { 36 | console.log(chalk_1.default ` 37 | {red ⚠️ {bold ${names.join(', ')}} hook${names.length > 1 ? 's' : ''} may need to be manually updated to be run via package manager.} 38 | 39 | {bold Examples:} 40 | jest → npx --no-install jest 41 | → yarn jest 42 | 43 | jest && eslint → npx --no-install jest && npx --no-install eslint 44 | → yarn jest && yarn eslint 45 | 46 | commitlint -E HUSKY_GIT_PARAMS → npx --no-install commitlint --edit $1 47 | → yarn commitlint --edit $1 48 | 49 | See {underline https://typicode.github.io/husky/#/?id=migrate-from-v4-to-v8} 50 | `); 51 | } 52 | } 53 | function run(removeV4Config) { 54 | var _a; 55 | const { hooks, filepath } = searchResult(); 56 | Object.entries(hooks).forEach(([name, script]) => { 57 | const file = `.husky/${name}`; 58 | husky_1.set(file, script); 59 | }); 60 | if (removeV4Config && filepath) { 61 | if (filepath.endsWith('package.json')) { 62 | const str = fs_1.default.readFileSync('package.json', 'utf-8'); 63 | const regex = /^[ ]+|\t+/m; 64 | const indent = (_a = regex.exec(str)) === null || _a === void 0 ? void 0 : _a[0]; 65 | const pkg = JSON.parse(str); 66 | delete pkg.husky; 67 | fs_1.default.writeFileSync('package.json', `${JSON.stringify(pkg, null, indent)}\n`); 68 | console.log('husky - deleted husky field from package.json'); 69 | } 70 | else { 71 | fs_1.default.unlinkSync(filepath); 72 | console.log(`husky - removed ${filepath}`); 73 | } 74 | } 75 | showManualUpdateMessage(hooks); 76 | } 77 | exports.run = run; 78 | --------------------------------------------------------------------------------