├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── README.md ├── bin └── yiya ├── index.js ├── package.json ├── src ├── default.ts ├── index.ts ├── publish.ts ├── publishHandler.ts ├── setup.ts ├── util │ ├── changelog-generate.ts │ ├── exec-shell.ts │ ├── update-version.ts │ └── yargs-optioin-generate.ts └── version.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | tsconfig.tsbuildinfo 3 | .history 4 | /node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tsconfig.tsbuildinfo 3 | yarn.lock 4 | tsconfig.json -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hanhh/yiya/af5734f9618d63646a7a5f2e01eb9db4e874f87a/CHANGELOG.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | yiya 2 | 一个全自动 NPM 包发布工具. 3 | 4 | yiyas 根据 (SemVer规范) 自动生成新版本号, 然后帮你自动修改版本号,生成changelog,生成commit message,打tag,推到GitHub,发布到 NPM 等。 5 | 6 | -------------------------------------------------------------------------------- /bin/yiya: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../index'); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const env = process.env.NODE_ENV; 2 | if (['development'].includes(env)) { 3 | require('./src/index'); 4 | } else { 5 | require('./lib/index'); 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiya", 3 | "version": "1.0.0", 4 | "description": "Automatically modify the version number in package.json, automatically generate changelog files, submit commit and tag, and automatically publish", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf ./lib && rm -f ./tsconfig.tsbuildinfo", 8 | "build-only": "tsc -b", 9 | "build": "yarn clean && yarn build-only", 10 | "watch": "yarn clean && tsc -w", 11 | "start": "NODE_ENV=development ts-node bin/yiya" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/Hanhh/yiya.git" 16 | }, 17 | "author": "", 18 | "license": "MIT", 19 | "bin": { 20 | "yiya": "bin/yiya" 21 | }, 22 | "preferGlobal": true, 23 | "dependencies": { 24 | "chalk": "^4.1.2", 25 | "child_process": "^1.0.2", 26 | "conventional-changelog": "^3.1.24", 27 | "inquirer": "^8.2.0", 28 | "inquirer-autocomplete-prompt": "^1.4.0", 29 | "semver": "^7.3.5", 30 | "yargs": "^17.2.1" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^16.11.1", 34 | "@types/yargs": "^17.0.4", 35 | "typescript": "^4.4.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/default.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import autocompletePrompt from 'inquirer-autocomplete-prompt'; 3 | 4 | const versions = ['Apple', 'Orange', 'Banana', 'Kiwi', 'Lichi', 'Grapefruit']; 5 | 6 | inquirer.registerPrompt('autocomplete', autocompletePrompt); 7 | 8 | function searchFood(answers: string, input: string) { 9 | input = input || ''; 10 | return new Promise((resolve) => { 11 | resolve(versions.filter(x=>x.toLowerCase().includes(input.toLowerCase()))) 12 | }); 13 | } 14 | 15 | export function showVersionDialog() { 16 | inquirer.prompt({ 17 | type: 'autocomplete', 18 | name: 'fruit', 19 | suggestOnly: true, 20 | message: 'What is your favorite fruit?', 21 | searchText: 'We are searching the internet for you!', 22 | emptyText: 'Nothing found!', 23 | default: versions[0], 24 | source: searchFood, 25 | pageSize: 10, 26 | validate: (val) => { 27 | return val ? true : 'Type something!'; 28 | }, 29 | }).then((answers: string) => { 30 | console.log(JSON.stringify(answers, null, 2)); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from "yargs"; 2 | import { publishCommand } from "./publish"; 3 | import { setCommand } from "./setup"; 4 | import { green, red } from "chalk"; 5 | import { showVersionDialog } from './default'; 6 | 7 | var argv = yargs 8 | 9 | .usage( 10 | "[options]\n\n Version format: MAJOR.MINOR.PATCH (see: https://semver.org/)" 11 | ) 12 | .command(publishCommand) 13 | .command(setCommand) 14 | .option("accessPublic", { 15 | alias: "accessPublic", 16 | demand: false, 17 | describe: "npm publish access=public", 18 | type: "string", 19 | }) 20 | .option("-m, remote [remote]", { 21 | alias: "remote", 22 | demand: false, 23 | describe: "remote and branch. format: `upstream/branch`", 24 | type: "string", 25 | }) 26 | .help("h") 27 | .alias("h", "help") 28 | .epilog( 29 | "\n Tip:\n You should run this script in the root directory of you project or run by npm scripts.\n Examples:\n" + 30 | ` ${green("$")} yiya --patch \n ${green( 31 | "$" 32 | )} yiya --prepatch \n ${green( 33 | "$" 34 | )} yiya --prepatch alpha \n ${green( 35 | "$" 36 | )} yiya --major --accessPublic \n ${green( 37 | "$" 38 | )} yiya --patch --remote upstream/branch \n` 39 | ) 40 | .fail((err) => { 41 | console.error(`${red(err)}`); 42 | }) 43 | .help().argv; 44 | 45 | const command = process.argv[2] as string; 46 | if(command) { 47 | if (!["publish", "set"].includes(command)) { 48 | yargs.showHelp(); 49 | } 50 | }else { 51 | showVersionDialog() 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/publish.ts: -------------------------------------------------------------------------------- 1 | import { CommandModule } from "yargs"; 2 | import { publishHanduler } from "./publishHandler"; 3 | export const publishCommand: CommandModule = { 4 | command: ["publish"], 5 | describe: 6 | "Automatically modify the version number in package.json, automatically generate changelog files, submit commit and tag, and automatically publish", 7 | builder: (yargs) => { 8 | yargs 9 | .option("patch", { 10 | alias: "patch", 11 | demand: false, 12 | describe: "version when you make backwards-compatible bug fixes.", 13 | type: "string", 14 | }) 15 | .option("minor", { 16 | alias: "minor", 17 | demand: false, 18 | describe: 19 | "version when you add functionality in a backwards-compatible manner", 20 | type: "string", 21 | }) 22 | .option("major", { 23 | alias: "major", 24 | demand: false, 25 | describe: "version when you make incompatible API changes", 26 | type: "string", 27 | }) 28 | .option("prepatch", { 29 | alias: "prepatch", 30 | demand: false, 31 | // default: "beta", 32 | describe: 33 | "increments the patch version, then makes a prerelease (default: beta)", 34 | type: "string", 35 | }) 36 | .option("preminor", { 37 | alias: "preminor", 38 | demand: false, 39 | // default: "beta", 40 | describe: 41 | "increments the minor version, then makes a prerelease (default: beta)", 42 | type: "string", 43 | }) 44 | .option("premajor", { 45 | alias: "premajor", 46 | demand: false, 47 | // default: "beta", 48 | describe: 49 | "increments the major version, then makes a prerelease (default: beta)", 50 | type: "string", 51 | }) 52 | .option("prerelease", { 53 | alias: "prerelease", 54 | demand: false, 55 | // default: "beta", 56 | describe: "increments version, then makes a prerelease (default: beta)", 57 | type: "string", 58 | }); 59 | return yargs; 60 | }, 61 | handler: publishHanduler, 62 | }; 63 | -------------------------------------------------------------------------------- /src/publishHandler.ts: -------------------------------------------------------------------------------- 1 | import { updatePackageJsonVersion } from "./util/update-version"; 2 | import { changelogGenerate } from "./util/changelog-generate"; 3 | import { execShell } from "./util/exec-shell"; 4 | import { runMain } from "module"; 5 | 6 | export const publishHanduler = (options) => { 7 | try { 8 | updatePackageJsonVersion(options).then((metadata) => { 9 | runMain(metadata) 10 | }); 11 | } catch (err) { 12 | throw err; 13 | } 14 | async function runMain(metadata) { 15 | await changelogGenerate(); 16 | execShell(metadata); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/setup.ts: -------------------------------------------------------------------------------- 1 | import { CommandModule } from 'yargs'; 2 | export const setCommand: CommandModule = { 3 | command: ['set'], 4 | describe: 'set release warehouse', 5 | builder: yargs => { 6 | yargs.option('repositority', { 7 | alias: 'r', 8 | desc: `Specify the release warehouse address of the package`, 9 | }); 10 | return yargs; 11 | }, 12 | handler: async argv => { 13 | const params: string[] = []; 14 | if (argv.mode) { 15 | params.push('--mode', `${argv.mode}`); 16 | } 17 | if (argv.docsDir) { 18 | params.push(`--docsDir`, `${argv.docsDir}`); 19 | } 20 | 21 | } 22 | }; -------------------------------------------------------------------------------- /src/util/changelog-generate.ts: -------------------------------------------------------------------------------- 1 | import conventionalChangelog from "conventional-changelog"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | 5 | export const changelogGenerate = async () => { 6 | const filePath = path.resolve(process.cwd(), "CHANGELOG.md"); 7 | const changelog = conventionalChangelog({ 8 | preset: "angular", 9 | }); 10 | const currentFileStr = fs.readFileSync(filePath, "utf8"); 11 | const res = await streamToBuffer(changelog); 12 | fs.writeFileSync(filePath, `${res.toString()}\n\n${currentFileStr}`); 13 | }; 14 | 15 | const streamToBuffer = (stream): Promise => { 16 | return new Promise((resolve, reject) => { 17 | let buffers = []; 18 | stream.on("error", reject); 19 | stream.on("data", (data) => buffers.push(data)); 20 | stream.on("end", () => resolve(Buffer.concat(buffers))); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /src/util/exec-shell.ts: -------------------------------------------------------------------------------- 1 | import { green, cyan } from "chalk"; 2 | import { exec } from "child_process"; 3 | export const execShell = (metadata) => { 4 | const shellList = [ 5 | `echo "\n${green("[ 1 / 3 ]")} ${cyan("Commit and push to remote")}\n"`, 6 | "git add .", 7 | `git commit -m "${metadata.prefix}${metadata.version}"`, 8 | "git push", 9 | `echo "\n${green("[ 2 / 3 ]")} ${cyan(`Tag and push tag to remote`)}\n"`, 10 | `git tag ${metadata.version}`, 11 | `git push`, 12 | `echo "\n${green("[ 3 / 3 ]")} ${cyan("Publish to NPM")}\n"`, 13 | ].join(' && '); 14 | return new Promise((resolve) => { 15 | const childExec = exec( 16 | shellList, 17 | { maxBuffer: 10000 * 10240 }, 18 | (err, stdout) => { 19 | if (err) { 20 | throw err; 21 | } else { 22 | resolve(stdout); 23 | } 24 | } 25 | ); 26 | childExec.stdout.pipe(process.stdout); 27 | childExec.stderr.pipe(process.stderr); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/util/update-version.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { green } from "chalk"; 3 | import path from "path"; 4 | import { getNewVersion } from "../version"; 5 | export const updatePackageJsonVersion = (options) => { 6 | const packageFile = path.resolve(process.cwd(), "package.json"); 7 | let packageFileData; 8 | let version; 9 | try { 10 | packageFileData = fs.readFileSync(packageFile, "utf8"); 11 | version = JSON.parse(packageFileData).version; 12 | } catch (err) { 13 | throw new Error("Can not find package.json in current work directory!"); 14 | } 15 | const metadata = getNewVersion(options, version); 16 | return new Promise((resolve, reject) => { 17 | fs.writeFile( 18 | packageFile, 19 | packageFileData.replace(version, metadata.version), 20 | "utf8", 21 | (err) => { 22 | if (err) reject(err); 23 | resolve(metadata); 24 | } 25 | ); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/util/yargs-optioin-generate.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs'; 2 | 3 | enum OptionType { 4 | Any = 'any', 5 | Array = 'array', 6 | Boolean = 'boolean', 7 | Number = 'number', 8 | String = 'string' 9 | } 10 | /** 11 | * An option description. `. 12 | */ 13 | interface Option { 14 | /** 15 | * The name of the option. 16 | */ 17 | name: string; 18 | /** 19 | * A short description of the option. 20 | */ 21 | description: string; 22 | /** 23 | * The type of option value. If multiple types exist, this type will be the first one, and the 24 | * types array will contain all types accepted. 25 | */ 26 | type: OptionType; 27 | /** 28 | * Aliases supported by this option. 29 | */ 30 | aliases: string[]; 31 | /** 32 | * Whether this option is required or not. 33 | */ 34 | required?: boolean; 35 | } 36 | export function yargsOptionsGenerate(yargs: yargs.Argv<{}>, list: Option[]) { 37 | list = list.slice(); 38 | 39 | while (list.length) { 40 | const item = list.pop(); 41 | yargs = yargs.option(item.name, { 42 | alias: item.aliases, 43 | array: item.type === OptionType.Array, 44 | boolean: item.type === OptionType.Boolean, 45 | description: item.description, 46 | demandOption: item.required, 47 | type: item.type !== OptionType.Any && item.type !== OptionType.Array ? item.type : undefined 48 | }); 49 | } 50 | return yargs; 51 | } -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | import semver from "semver"; 2 | import { red } from "chalk"; 3 | 4 | interface MetaData { 5 | version?: string; 6 | prefix?: string; 7 | } 8 | export const getNewVersion = (options, version) => { 9 | const semverList = [ 10 | ["patch", "Bump version "], 11 | ["minor", "Release version "], 12 | ["major", "Release major version "], 13 | ]; 14 | const preSemverList = ["prepatch", "preminor", "premajor", "prerelease"]; 15 | 16 | const metadata: MetaData = {}; 17 | 18 | const increase = (v, release, identifier) => { 19 | return semver.inc(v, release, identifier); 20 | }; 21 | 22 | semverList.forEach((sem) => { 23 | if (options[sem[0]] !== undefined) { 24 | if (metadata.version) { 25 | console.error( 26 | `${red( 27 | "You specified more than one semver type, please specify only one!" 28 | )}` 29 | ); 30 | process.exit(1); 31 | } 32 | const identifier = 33 | typeof options[sem[0]] === "boolean" ? "beta" : options[sem[0]]; 34 | metadata.version = increase(version, sem[0], identifier); 35 | metadata.prefix = sem[1]; 36 | } 37 | }); 38 | 39 | preSemverList.forEach((sem) => { 40 | if (options[sem] !== undefined) { 41 | if (metadata.version) { 42 | console.error( 43 | `${red( 44 | "You specified more than one semver type, please specify only one!" 45 | )}` 46 | ); 47 | process.exit(1); 48 | } 49 | const identifier = 50 | typeof options[sem] === "boolean" ? "beta" : options[sem]; 51 | metadata.version = increase(version, sem, identifier); 52 | metadata.prefix = `Prerelease ${identifier} version `; 53 | } 54 | }); 55 | return metadata; 56 | }; 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "alwaysStrict": true, 6 | "rootDir": "src", 7 | "baseUrl": ".", 8 | "target": "ES2018", 9 | "module": "commonjs", 10 | "declaration": true, 11 | "declarationMap": true, 12 | "sourceMap": true, 13 | "composite": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "include": ["src"] 18 | } --------------------------------------------------------------------------------