All files / git / changelog.ts

100.00% Branches 0/0
11.54% Lines 3/26
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
 
 
x1
x1
x1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

































































// Imports
import type { Arrayable, Nullable } from "@libs/typing"
import { assert } from "@std/assert"
import * as semver from "@std/semver"
import * as git from "./mod.ts"
export type { Arrayable, Nullable }
export type * from "@std/semver"

/**
 * Generate a changelog based on the commits since the last version bump.
 *
 * It is automatically determined by the list of commits since the last edit of the "version" key.
 * Commits must follow the conventional commits syntax (non-conventional commits are ignored).
 */
export function changelog(path: string, { scopes = [], minor = ["feat"], patch = ["fix", "docs", "perf", "refactor", "chore"], ...options } = {} as ChangelogOptions): Changelog {
  // Find the base commit (last version bump)
  const base = git.blame(path).find(({ content }) => /"version": "\d\.\d\.\d.*"/.test(content))
  assert(base, `Could not find "version" key in ${path}`)

  // Parse the version
  const version = semver.parse(base.content.match(/"version": "(?<version>\d\.\d\.\d.*)"/)?.groups?.version ?? "")
  const commits = git.log(base.sha, { filter: { conventional: true, scopes } })
  const result = { version: { bump: null, current: version, next: version }, changelog: "" } as Changelog

  // Bump the version
  if (commits.some(({ breaking }) => breaking)) {
    result.version.bump = "major"
    result.version.next = semver.increment(version, result.version.bump)
  } else if (commits.some(({ type }) => minor.includes(type as string))) {
    result.version.bump = "minor"
    result.version.next = semver.increment(version, result.version.bump)
  } else if (commits.some(({ type }) => patch.includes(type as string))) {
    result.version.bump = "patch"
    result.version.next = semver.increment(version, result.version.bump)
  }

  // Write the new version
  if (options.write) {
    const content = Deno.readTextFileSync(path)
    Deno.writeTextFileSync(path, content.replace(/"version": "\d\.\d\.\d.*"/, `"version": "${semver.format(result.version.next)}"`))
  }

  // Generate the changelog
  result.changelog = commits.map(({ summary }) => summary).join("\n")
  return result
}

/** Options for {@linkcode changelog()}. */
export type ChangelogOptions = {
  /** The scopes to filter. */
  scopes?: Arrayable<string>
  /** The type of commits that increase the "patch" component of semver. */
  patch?: Arrayable<string>
  /** The type of commits that increase the "minor" component of semver. */
  minor?: Arrayable<string>
  /** Whether to update the version in the file. */
  write?: boolean
}

/** Changelog. */
export type Changelog = {
  version: {
    bump: Nullable<"major" | "minor" | "patch">
    current: semver.SemVer
    next: semver.SemVer
  }
  changelog: string
}