Skip to content

Commit

Permalink
feat(cli): ✨ added cli for working with devmoji 🚀
Browse files Browse the repository at this point in the history
  • Loading branch information
folke committed Jan 14, 2020
1 parent f6e10d6 commit 8f16492
Show file tree
Hide file tree
Showing 7 changed files with 406 additions and 58 deletions.
136 changes: 136 additions & 0 deletions packages/devmoji/src/cli.ts
@@ -0,0 +1,136 @@
import { Config } from "./config"
import { Command } from "commander"
import readline = require("readline")
import { Devmoji } from "./devmoji"
import chalk from "chalk"
import { ConventionalCommits } from "./conventional-commits"
import * as path from "path"
import * as fs from "fs"

export class Cli {
commits: ConventionalCommits
constructor(public program: Command, public devmoji: Devmoji) {
this.commits = new ConventionalCommits(devmoji)
}

format(text: string, format = "unicode", processCommit = false) {
if (processCommit) text = this.commits.format(text)
switch (format) {
case "unicode":
return text
case "shortcode":
return this.devmoji.demojify(text)
case "devmoji":
return this.devmoji.devmojify(text)
}
throw `Invalid format '${format}'`
}

list() {
console.log(chalk.magenta.underline("all configured devmoji"))
for (const code of this.devmoji.config.codes.values()) {
console.log(
this.devmoji.get(code.emoji),
" ",
chalk.blue(`:${code.code}:`.padEnd(10)),
code.description
)
}
}

error(msg: string) {
console.error(chalk.red("[error] ") + msg)
process.exit(1)
}

gitRoot(cwd = process.cwd()): string | undefined {
if (cwd == "/") return undefined
const p = path.posix.resolve(cwd, "./.git")
if (fs.existsSync(p) && fs.lstatSync(p).isDirectory()) return p
return this.gitRoot(path.resolve(cwd, "../"))
}

static async create(argv = process.argv, exitOverride = false) {
const program = new Command()
if (exitOverride) program.exitOverride()
program
.version(require("../package.json").version)
.option("-g|--config <file>", "location of the devmoji.config.js file")
.option("-l|--list", "list all known devmojis")
.option(
"-t|--text <text>",
"text to format. reads from stdin when omitted"
)
.option(
"-f|--format <format>",
"format should be one of: unicode, shortcode, devmoji",
"unicode"
)
.option(
"-c|--commit",
"automatically add a devmoji to the conventional commit header",
true
)
.option(
"-e|--edit",
"read last commit message from the specified file or fallbacks to ./.git/COMMIT_EDITMSG"
)
.parse(argv)
// console.log(program.opts())
const config = await Config.load(program.config)
return new Cli(program, new Devmoji(config))
}

run() {
if (this.program.list) return this.list()

if (this.program.text)
return console.log(
this.format(this.program.text, this.program.format, this.program.commit)
)

if (this.program.edit) {
let commitMsgFile = this.gitRoot()
if (commitMsgFile) {
commitMsgFile = path.resolve(commitMsgFile, "COMMIT_EDITMSG")
}
if (commitMsgFile && fs.existsSync(commitMsgFile)) {
let text = fs.readFileSync(commitMsgFile, "utf-8")
text = this.format(text, this.program.format, this.program.commit)
fs.writeFileSync(commitMsgFile, text, "utf-8")
return console.log(text)
} else {
this.error("Couldn't find .git/COMMIT_EDITMSG")
}
return
}

if (!process.stdin.isTTY) {
const rl = readline.createInterface({
input: process.stdin,
terminal: false,
})

let lineNumber = 0
rl.on("line", line => {
try {
console.log(this.format(line, this.program.format, !lineNumber++))
} catch (err) {
this.error(err)
}
})
return
}

this.program.outputHelp()
return process.exit(1)
}
}

export function run(argv = process.argv) {
Cli.create(argv).then(cli => cli.run())
}

if (module === require.main) {
run()
}
49 changes: 49 additions & 0 deletions packages/devmoji/src/codes.ts
@@ -0,0 +1,49 @@
export type TDevmoji = { code: string; emoji: string; description: string }

export const codes: TDevmoji[] = [
{ code: "feat", description: "a new feature", emoji: "sparkles" },
{ code: "fix", description: "a bug fix", emoji: "bug" },
{ code: "docs", description: "documentation only changes", emoji: "books" },
{
code: "style",
description:
"changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",
emoji: "art",
},
{
code: "refactor",
description: "a code change that neither fixes a bug nor adds a feature",
emoji: "hammer",
},
{
code: "perf",
description: "a code change that improves performance",
emoji: "zap",
},
{
code: "test",
description: "adding missing or correcting existing tests",
emoji: "rotating_light",
},
{
code: "chore",
description:
"changes to the build process or auxiliary tools and libraries such as documentation generation",
emoji: "wrench",
},
{
code: "build",
description: "changes related to build processes",
emoji: "package",
},
{
code: "ci",
description: "updates to the continuous integration system",
emoji: "construction_worker",
},
{
code: "release",
description: "code deployment or publishing to external repositories",
emoji: "rocket",
},
]
49 changes: 49 additions & 0 deletions packages/devmoji/src/config.ts
@@ -0,0 +1,49 @@
import { codes, TDevmoji } from "./codes"
import path = require("path")
import fs = require("fs")

export class Config {
codes = new Map<string, TDevmoji>()
emojis = new Map<string, TDevmoji>()

constructor(config?: { codes: TDevmoji[] }) {
codes.forEach(c => this.codes.set(c.code, c))

if (config?.codes) {
for (let code of config.codes) {
if (!code.code)
throw `Missing property 'code' for ${JSON.stringify(code)}`
if (!code.emoji)
throw `Missing property 'emoji' for ${JSON.stringify(code)}`
code = {
...{
description: "",
},
...this.codes.get(code.code),
...code,
}
this.codes.set(code.code, code)
}
}
for (const code of this.codes.values()) {
this.emojis.set(code.emoji, code)
}
}

static async load(configFile?: string, cwd = process.cwd()) {
if (configFile && !fs.existsSync(configFile))
throw `Config file not found ${configFile}`

if (!configFile) {
const defaultFile = path.resolve(cwd, "./devmoji.config.js")
if (fs.existsSync(defaultFile)) configFile = defaultFile
}

if (configFile) {
configFile = path.resolve(cwd, configFile)
return new Config(await import(configFile))
}

return new Config()
}
}
22 changes: 22 additions & 0 deletions packages/devmoji/src/conventional-commits.ts
@@ -0,0 +1,22 @@
import { Devmoji } from "./devmoji"

export class ConventionalCommits {
constructor(public devmoji: Devmoji) {}

format(text: string) {
text = this.devmoji.devmojify(text)
const regex = /^(?<type>[a-z]+)(?:\((?<scope>[a-z]+)\))?:\s*/g
const match = regex.exec(text)
if (match) {
const [all, type, scope] = match
const code = this.devmoji.get(type)
if (!code.startsWith(":")) {
text =
text.slice(0, all.length).trimRight() +
` ${code} ` +
text.slice(all.length).trimLeft()
}
}
return this.devmoji.emojify(text)
}
}
40 changes: 40 additions & 0 deletions packages/devmoji/src/devmoji.ts
@@ -0,0 +1,40 @@
import { Config } from "./config"
import { emoji } from "./emoji"

export class Devmoji {
shortcodeRegex = /:([a-zA-Z0-9_\-+]+):/g
unicodeRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g

constructor(public config: Config) {}

get(code: string): string {
const ret = this.config.codes.get(code)
if (ret) return this.get(ret.emoji)
return emoji.get(code)
}

emojify(text: string): string {
return text.replace(this.shortcodeRegex, (match, code) => {
return this.get(code)
})
}

demojify(text: string): string {
return text.replace(this.unicodeRegex, s => {
return emoji.getCode(s)
})
}

devmojify(text: string): string {
text = this.demojify(text)
return text.replace(this.shortcodeRegex, (match, code) => {
const unicode = emoji.get(code)
const codes = emoji.getCodes(unicode)
for (const c of codes) {
const ret = this.config.emojis.get(emoji.unwrap(c))
if (ret) return `:${ret.code}:`
}
return match
})
}
}
46 changes: 46 additions & 0 deletions packages/devmoji/src/emoji.ts
@@ -0,0 +1,46 @@
import * as emojis from "./data/github.emoji.json"
import * as gitmojis from "./data/gitmoji.emoji.json"

class Emoji {
codes = new Map<string, string>()
emojis = new Map<string, string[]>()

constructor() {
for (const [code, emoji] of Object.entries(emojis)) {
this.codes.set(this.wrap(code), emoji)
this.emojis.set(emoji, [
...(this.emojis.get(emoji) || []),
this.wrap(code),
])
}
// Make sure the shortcodes for gitmojis are the first ones in the list
for (const { emoji, code } of gitmojis.gitmojis) {
const codes = new Set(this.getCodes(emoji))
codes.delete(code)
this.emojis.set(emoji, [code, ...codes])
}
}

wrap(code: string) {
return code.startsWith(":") ? code : `:${code}:`
}

unwrap(code: string) {
return code.startsWith(":") ? code.slice(1, code.length - 1) : code
}

getCodes(emoji: string): string[] {
return this.emojis.get(emoji) || [emoji]
}

getCode(emoji: string): string {
return this.getCodes(emoji)[0]
}

get(code: string): string {
code = this.wrap(code)
return this.codes.get(code) || code
}
}

export const emoji = new Emoji()

0 comments on commit 8f16492

Please sign in to comment.