From 6b9961dfa5f831167a3b87b26f709faa6c5d94cb Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 23 Aug 2023 18:35:26 +0200 Subject: [PATCH] Refactor to move implementation to `lib/` --- .gitignore | 4 +- complex-types.d.ts => index.d.ts | 5 +- index.js | 125 +------------------------------ lib/index.js | 123 ++++++++++++++++++++++++++++++ package.json | 2 +- tsconfig.json | 2 +- 6 files changed, 132 insertions(+), 129 deletions(-) rename complex-types.d.ts => index.d.ts (87%) create mode 100644 lib/index.js diff --git a/.gitignore b/.gitignore index bdf6c59..fcb2607 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ coverage/ node_modules/ .DS_Store -test/*.d.ts -index.d.ts +*.d.ts *.log yarn.lock +!/index.d.ts diff --git a/complex-types.d.ts b/index.d.ts similarity index 87% rename from complex-types.d.ts rename to index.d.ts index 80773cd..a974654 100644 --- a/complex-types.d.ts +++ b/index.d.ts @@ -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 diff --git a/index.js b/index.js index 7349302..6a4c25d 100644 --- a/index.js +++ b/index.js @@ -1,123 +1,2 @@ -/** - * @typedef {import('./complex-types.js')} DoNotTouchItIncludesAugmentation - * - * @callback Format - * Format function. - * @param {Array} authors - * List of authors. - * @returns {string} - * Formatted authors. - * - * @typedef Options - * Configuration (optional). - * @property {string|Array} [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}} */ - // @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} */ - 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} */ ( - 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' diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..d38ff9f --- /dev/null +++ b/lib/index.js @@ -0,0 +1,123 @@ +/** + * @typedef {import('../index.js')} DoNotTouchItIncludesAugmentation + * + * @callback Format + * Format function. + * @param {Array} authors + * List of authors. + * @returns {string} + * Formatted authors. + * + * @typedef Options + * Configuration (optional). + * @property {string|Array} [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}} */ + // @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} */ + 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} */ ( + 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 + } +} diff --git a/package.json b/package.json index 0c4dbb4..b0ea51a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "main": "index.js", "types": "index.d.ts", "files": [ - "complex-types.d.ts", + "lib/", "index.d.ts", "index.js" ], diff --git a/tsconfig.json b/tsconfig.json index 81b8c39..ad1496e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "target": "es2020" }, "exclude": ["coverage/", "node_modules/"], - "include": ["**/*.js", "complex-types.d.ts"] + "include": ["**/*.js", "index.d.ts"] }