staticsitegenerators
Version:
A comprehensive, partially automatically generated comparison of static site generators with some minimal meta data about them
180 lines (166 loc) • 5.29 kB
text/typescript
/* eslint camelcase:0 */
// Imports
import { RawEntry, HydratedEntry } from './types.js'
import naturalCompare from 'string-natural-compare'
import { validateCredentials, getGitHubRepositories } from '@bevry/github-api'
import arrangeKeys from 'arrangekeys'
import crypto from 'node:crypto'
/** The preferred order of keys when arranging entry objects */
const keyorder =
'id name github gitlab bitbucket website license language description created_at updated_at abandoned is extensible stars forks watchers'
/**
* Sort an array of entries by name and then by GitHub repository name.
* @param data The array of entries with name and optional github properties to sort
* @returns Sorted array of entries
*/
function sort<T extends { name: string; github?: string }>(data: T[]) {
return data.sort(
(a, b) =>
naturalCompare(a.name, b.name, {
caseInsensitive: true,
}) ||
naturalCompare(a.github, b.github, {
caseInsensitive: true,
}),
)
}
/**
* Compare two values for case-insensitive equality, handling type coercion.
* @param a The first value to compare
* @param b The second value to compare
* @returns True if the values are considered equal, false otherwise
*/
function same(a: unknown, b: unknown): boolean {
const ta = typeof a,
tb = typeof b
if (ta === tb) {
if (ta === 'string') {
return (a as string).toLowerCase() === (b as string).toLowerCase()
}
return a === b
}
/* eslint eqeqeq:0 */
return a == b
}
export interface HydrateOptions {
/** Whether to perform corrective actions on the data */
corrective?: boolean
/** Cache duration in milliseconds */
cache?: number
/** Logging function that accepts a log level and arguments */
log?: (logLevel: string, ...args: unknown[]) => void // eslint-disable-line
}
/** The result of the hydrate operation containing both raw and hydrated data */
export interface HydrateReturn {
/** The raw entries after processing */
raw: RawEntry[]
/** The hydrated entries with additional GitHub metadata */
hydrated: HydratedEntry[]
}
/**
* Trim redundant data from the listing and enhance with GitHub API data.
* @param data An array of raw entries to hydrate with GitHub metadata
* @param opts Configuration options for the hydration process including corrective mode, cache duration, and logging function
* @returns A promise resolving to both raw and hydrated entry arrays
*/
export async function hydrate(
data: RawEntry[],
opts: HydrateOptions = {},
): Promise<HydrateReturn> {
validateCredentials()
if (opts.corrective == null) opts.corrective = false
if (opts.cache == null) opts.cache = 1000 * 60 * 60 * 24 // one day
const rawMap: { [id: string]: RawEntry } = {}
const hydratedMap: { [id: string]: HydratedEntry } = {}
const githubRepos: string[] = []
data.forEach(function (entry, index) {
delete entry.id
const key = (entry.github && entry.github.toLowerCase()) || index
rawMap[key] = Object.assign({}, arrangeKeys(entry, keyorder))
hydratedMap[key] = Object.assign(
{
id: crypto
.createHash('md5')
.update(
JSON.stringify({
name: entry.name,
website: entry.website,
github: entry.github,
}),
)
.digest('hex'),
},
arrangeKeys(entry, keyorder),
)
if (entry.github) {
githubRepos.push(entry.github)
}
})
// Log
if (opts.log) {
opts.log(
'info',
`Fetching the github information, all ${githubRepos.length} of them`,
)
}
// Enhance with github data
const repos = await getGitHubRepositories(githubRepos)
for (const github of repos) {
// Prepare
const key = github.full_name.toLowerCase()
const raw = rawMap[key]
const hydrated = hydratedMap[key]
// Confirm existence as name may have changed from the listing, for example a repo rename
if (raw == null) {
if (opts.log) {
opts.log('warn', `${github.full_name} is missing, likely due to rename`)
}
continue // skip
}
// Apply github fields
const fields: Partial<HydratedEntry> = {
description: github.description,
language: github.language,
license: github.license && github.license.key,
website:
github.homepage &&
github.homepage.toLowerCase().includes(`github.com/${key}`) &&
github.homepage,
stars: github.stargazers_count,
watchers: github.watchers_count,
forks: github.forks_count,
// @ts-expect-error typescript is wrong
created_at: github.created_at,
// @ts-expect-error typescript is wrong
updated_at: github.updated_at,
}
for (const [key, value] of Object.entries(fields)) {
const rawValue = raw[key]
if (value) {
if (opts.corrective && rawValue && value && same(rawValue, value)) {
if (opts.log) {
opts.log(
'note',
`trimming ${key} on ${github.full_name} as it is the same as the github data: ${value}`,
)
}
delete raw[key] // eslint-disable-line
}
if (hydrated[key] == null) {
if (opts.log) {
opts.log(
'info',
`added ${key} on ${github.full_name} from the github data`,
)
}
hydrated[key] = value
}
}
}
hydratedMap[key] = arrangeKeys(hydrated, keyorder)
}
return {
hydrated: sort(Object.keys(hydratedMap).map((k) => hydratedMap[k])),
raw: sort(Object.keys(rawMap).map((k) => rawMap[k])),
}
}