Skip to content

Commit

Permalink
Refactor to move implementation to lib/
Browse files Browse the repository at this point in the history
  • Loading branch information
wooorm committed Aug 23, 2023
1 parent 04af5ad commit 6b9961d
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 129 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
coverage/
node_modules/
.DS_Store
test/*.d.ts
index.d.ts
*.d.ts
*.log
yarn.lock
!/index.d.ts
5 changes: 3 additions & 2 deletions complex-types.d.ts → index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Need a random export to turn this into a module?
export {}
export type {Format, Options} from './lib/index.js'

export {default} from './lib/index.js'

declare module 'vfile' {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
Expand Down
125 changes: 2 additions & 123 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,123 +1,2 @@
/**
* @typedef {import('./complex-types.js')} DoNotTouchItIncludesAugmentation
*
* @callback Format
* Format function.
* @param {Array<string>} authors
* List of authors.
* @returns {string}
* Formatted authors.
*
* @typedef Options
* Configuration (optional).
* @property {string|Array<string>} [locales='en']
* Locale(s) to use to join authors and sort their names.
* @property {number} [limit=3]
* Maximum number of authors to include.
* Set to `-1` to not limit authors.
* @property {string} [authorRest='others']
* Text to use to label more authors when abbreviating.
* @property {Format} [format]
* Alternative format function to use.
* Is given a list of abbreviated author names.
* If the authors had to be abbreviated, the last author is instead replaced
* by `authorRest`.
* Set `limit: -1` to receive all author names.
*/

import {exec} from 'node:child_process'
import {promisify} from 'node:util'
import {csvParse} from 'd3-dsv'

const own = {}.hasOwnProperty

/**
* Plugin to infer some `meta` from Git.
*
* This plugin sets `file.data.meta.published` to the date a file was first
* committed, `file.data.meta.modified` to the date a file was last committed,
* and `file.data.meta.author` to an abbreviated list of top authors of the
* file.
*
* @type {import('unified').Plugin<[(Options | null | undefined)?]>}
*/
export default function unifiedInferGitMeta(options) {
const settings = options || {}
const locales = settings.locales || 'en'
const limit = settings.limit || 3
const rest = settings.authorRest || 'others'
const collator = new Intl.Collator(locales)
/** @type {{format(items: Array<string>): string}} */
// @ts-expect-error: TS doesn’t know about `ListFormat` yet.
const listFormat = new Intl.ListFormat(locales)

// eslint-disable-next-line complexity
return async (_, file) => {
const {stdout} = await promisify(exec)(
'git log --all --follow --format="%aN,%aE,\\"%cD\\"" "' + file.path + '"',
{cwd: file.cwd}
)

const commits = [
...csvParse('name,email,date\n' + stdout, (d) => ({
// Git should yield clean data, but just to be sure.
/* c8 ignore next 3 */
date: new Date(d.date || ''),
name: d.name || '',
email: d.email || ''
}))
]

/** @type {Record<string, {name: string, commits: number}>} */
const byEmail = {}
let index = -1
let published = new Date()
let modified = new Date()

while (++index < commits.length) {
const commit = commits[index]
const current = (byEmail[commit.email] || {}).commits || 0

if (index === 0) modified = commit.date
if (index === commits.length - 1) published = commit.date

byEmail[commit.email] = {name: commit.name, commits: current + 1}
}

/** @type {Array<{email: string, name: string, commits: number}>} */
const sortedCommits = []
/** @type {string} */
let email

for (email in byEmail) {
if (own.call(byEmail, email)) {
sortedCommits.push({email, ...byEmail[email]})
}
}

const sortedAuthors = sortedCommits
.sort((a, b) => b.commits - a.commits || collator.compare(a.name, b.name))
.map((d) => d.name)
const abbreviatedAuthors =
limit > -1 && sortedAuthors.length > limit
? limit === 1
? [sortedAuthors[0]]
: [...sortedAuthors.slice(0, limit - 1), rest]
: sortedAuthors

/** @type {string|undefined} */
const author =
(settings.format
? settings.format(abbreviatedAuthors)
: listFormat.format(abbreviatedAuthors)) || undefined

const matter = /** @type {Record<string, unknown>} */ (
file.data.matter || {}
)
const meta = file.data.meta || (file.data.meta = {})

if (!matter.published && !meta.published) meta.published = published
if (!matter.modified && !meta.modified) meta.modified = modified
if (author && !matter.author && !meta.author) meta.author = author
}
}
// Note: types exposed from `index.d.ts`.
export {default} from './lib/index.js'
123 changes: 123 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* @typedef {import('../index.js')} DoNotTouchItIncludesAugmentation
*
* @callback Format
* Format function.
* @param {Array<string>} authors
* List of authors.
* @returns {string}
* Formatted authors.
*
* @typedef Options
* Configuration (optional).
* @property {string|Array<string>} [locales='en']
* Locale(s) to use to join authors and sort their names.
* @property {number} [limit=3]
* Maximum number of authors to include.
* Set to `-1` to not limit authors.
* @property {string} [authorRest='others']
* Text to use to label more authors when abbreviating.
* @property {Format} [format]
* Alternative format function to use.
* Is given a list of abbreviated author names.
* If the authors had to be abbreviated, the last author is instead replaced
* by `authorRest`.
* Set `limit: -1` to receive all author names.
*/

import {exec} from 'node:child_process'
import {promisify} from 'node:util'
import {csvParse} from 'd3-dsv'

const own = {}.hasOwnProperty

/**
* Plugin to infer some `meta` from Git.
*
* This plugin sets `file.data.meta.published` to the date a file was first
* committed, `file.data.meta.modified` to the date a file was last committed,
* and `file.data.meta.author` to an abbreviated list of top authors of the
* file.
*
* @type {import('unified').Plugin<[(Options | null | undefined)?]>}
*/
export default function unifiedInferGitMeta(options) {
const settings = options || {}
const locales = settings.locales || 'en'
const limit = settings.limit || 3
const rest = settings.authorRest || 'others'
const collator = new Intl.Collator(locales)
/** @type {{format(items: Array<string>): string}} */
// @ts-expect-error: TS doesn’t know about `ListFormat` yet.
const listFormat = new Intl.ListFormat(locales)

// eslint-disable-next-line complexity
return async (_, file) => {
const {stdout} = await promisify(exec)(
'git log --all --follow --format="%aN,%aE,\\"%cD\\"" "' + file.path + '"',
{cwd: file.cwd}
)

const commits = [
...csvParse('name,email,date\n' + stdout, (d) => ({
// Git should yield clean data, but just to be sure.
/* c8 ignore next 3 */
date: new Date(d.date || ''),
name: d.name || '',
email: d.email || ''
}))
]

/** @type {Record<string, {name: string, commits: number}>} */
const byEmail = {}
let index = -1
let published = new Date()
let modified = new Date()

while (++index < commits.length) {
const commit = commits[index]
const current = (byEmail[commit.email] || {}).commits || 0

if (index === 0) modified = commit.date
if (index === commits.length - 1) published = commit.date

byEmail[commit.email] = {name: commit.name, commits: current + 1}
}

/** @type {Array<{email: string, name: string, commits: number}>} */
const sortedCommits = []
/** @type {string} */
let email

for (email in byEmail) {
if (own.call(byEmail, email)) {
sortedCommits.push({email, ...byEmail[email]})
}
}

const sortedAuthors = sortedCommits
.sort((a, b) => b.commits - a.commits || collator.compare(a.name, b.name))
.map((d) => d.name)
const abbreviatedAuthors =
limit > -1 && sortedAuthors.length > limit
? limit === 1
? [sortedAuthors[0]]
: [...sortedAuthors.slice(0, limit - 1), rest]
: sortedAuthors

/** @type {string|undefined} */
const author =
(settings.format
? settings.format(abbreviatedAuthors)
: listFormat.format(abbreviatedAuthors)) || undefined

const matter = /** @type {Record<string, unknown>} */ (
file.data.matter || {}
)
const meta = file.data.meta || (file.data.meta = {})

if (!matter.published && !meta.published) meta.published = published
if (!matter.modified && !meta.modified) meta.modified = modified
if (author && !matter.author && !meta.author) meta.author = author
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"main": "index.js",
"types": "index.d.ts",
"files": [
"complex-types.d.ts",
"lib/",
"index.d.ts",
"index.js"
],
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
"target": "es2020"
},
"exclude": ["coverage/", "node_modules/"],
"include": ["**/*.js", "complex-types.d.ts"]
"include": ["**/*.js", "index.d.ts"]
}

0 comments on commit 6b9961d

Please sign in to comment.