@modernpoacher/deps
Version:
Update NPM package dependencies from the command line
592 lines (507 loc) • 12.7 kB
JavaScript
import debug from 'debug'
import {
resolve,
dirname
} from 'node:path'
import {
glob
} from 'glob'
import {
Command
} from 'commander'
import '#deps/src/common/debug'
import {
VERSION,
PLATFORM
} from '#deps/src/common/env'
import {
MESSAGE,
AUTHOR,
catGitRefsRemotesOriginHead,
awkGitRemoteShowOriginHead,
gitRevParseShowTopLevel,
gitRevParseAbbrevRefHead,
gitPull,
gitPush,
gitPushTags,
gitAdd,
gitCommit
} from '#deps/src/common/git'
import {
DIRECTORY,
REGISTRY,
getIgnore,
getAuthor,
getMessage
} from '#deps/src/common'
import {
handleError,
rmrf,
npmi,
deps,
hasConfiguration,
getConfiguration,
hasPackage,
getPackage
} from '#deps/bin/common'
const log = debug('@modernpoacher/deps')
const info = debug('@modernpoacher/deps:info')
log(`\`execute\` (${VERSION} - ${PLATFORM}) is awake`)
/**
* @param {{ code?: number, message?: string }} [e]
*/
function handleCommandError (e = {}) {
const {
code = 0
} = e
if (code !== 128) handleError(e)
}
/**
* @param {string} directory
* @returns {Promise<string>}
*/
async function getDefaultBranch (directory) {
const branch = await catGitRefsRemotesOriginHead(directory)
if (branch) return branch
return (
await awkGitRemoteShowOriginHead(directory)
)
}
/**
* @param {string} directory
* @returns {Promise<string>}
*/
async function getCurrentBranch (directory) {
return (
await gitRevParseAbbrevRefHead(directory)
)
}
/**
* @param {string} directory
* @returns {Promise<boolean>}
*/
async function isHeadDefaultBranch (directory) {
const defaultBranch = await getDefaultBranch(directory)
const currentBranch = await getCurrentBranch(directory)
return (
defaultBranch === currentBranch
)
}
/**
* @function getIgnoreFromConfiguration
*
* Interrogates `.depsrc` or `.depsrc.json` for an `ignore` value
*
* @param {string} directory
* @returns {Promise<boolean>}
*/
async function getIgnoreFromConfiguration (directory) {
log('getIgnoreFromConfiguration')
const CONFIGURATION = (
await hasConfiguration(directory)
? await getConfiguration(directory)
: {}
)
return (
getIgnore(CONFIGURATION)
)
}
/**
* @function getAuthorFromConfiguration
*
* Interrogates `.depsrc` or `.depsrc.json` for an `author` value
*
* @param {string} directory
* @returns {Promise<string | null>}
*/
async function getAuthorFromConfiguration (directory) {
log('getAuthorFromConfiguration')
const CONFIGURATION = (
await hasConfiguration(directory)
? await getConfiguration(directory)
: {}
)
return (
getAuthor(CONFIGURATION)
)
}
/**
* @function getAuthorFromPackage
*
* Interrogates `package.json` for an `author` value
*
* @param {string} directory
* @returns {Promise<string | null>}
*/
async function getAuthorFromPackage (directory) {
log('getAuthorFromPackage')
const PACKAGE = (
await hasPackage(directory)
? await getPackage(directory)
: {}
)
return (
getAuthor(PACKAGE)
)
}
/**
* @function getMessageFromConfiguration
*
* Interrogates `.depsrc` or `.depsrc.json` for a `message` value
*
* @param {string} directory
* @returns {Promise<string | null>}
*/
async function getMessageFromConfiguration (directory) {
log('getMessageFromConfiguration')
const CONFIGURATION = (
await hasConfiguration(directory)
? await getConfiguration(directory)
: {}
)
return (
getMessage(CONFIGURATION)
)
}
/**
* @function toAuthor
*
* Ensures that the `author` is set either from the command line,
* configuration, package, or a default
*
* Since it can be null `author` may not default in function arguments
*
* @param {string | { name: string; email: string }} author
* @param {string} directory
* @returns {Promise<string | { name: string; email: string }>}
*/
async function toAuthor (author, directory) {
return author || await getAuthorFromConfiguration(directory) || await getAuthorFromPackage(directory) || AUTHOR
}
/**
* @function toMessage
*
* Ensures that the `message` is set either from the command line,
* configuration, or a default
*
* Since it can be null `message` may not default in function arguments
*
* @param {string | null} message
* @param {string} directory
* @returns {Promise<string>}
*/
async function toMessage (message, directory) {
return message || await getMessageFromConfiguration(directory) || MESSAGE
}
/**
* @param {string | undefined} v
* @returns {boolean}
*/
const filterDeps = (v) => Boolean(v) // de-falsy
/**
* @param {string[]} a
* @param {string} v
* @returns {string[]}
*/
const reduceDeps = (a, v = '') => a.includes(v) ? a : a.concat(v) // de-dupe
/**
* @param {string[]} directories
*/
function * genDirsList (directories = []) {
while (directories.length) yield directories.shift()
}
/**
* @param {string} directory
* @returns {Promise<string[]>}
*/
async function getPathList (directory) {
log('getPathList')
const array = await glob(`${directory}/*/package.json`)
return (
array
.map(dirname)
.sort()
)
}
/**
* @param {Array<string | undefined>} [directories]
* @returns {Promise<string[]>}
*/
async function getDepsList (directories = []) {
log('getDepsList')
/**
* Transform `directories` to create `alpha`
*/
const alpha = (
directories
.filter(filterDeps)
.reduce(reduceDeps, [])
.sort()
)
try {
/**
* Recreate `directories` from `alpha`
*
* @type {Array<string | undefined>}
*/
const directories = (
await Promise.all(
alpha
.map(mapRevParseShowTopLevel)
)
)
/**
* Transform `directories` to create `omega`
*/
const omega = (
directories
.filter(filterDeps)
.reduce(reduceDeps, [])
.sort()
)
return omega
} catch (e) {
if (e instanceof Error) handleError(e)
}
return []
}
/**
* @param {string} directory
* @returns {Promise<string | undefined>}
*/
async function mapRevParseShowTopLevel (directory) {
log('mapRevParseShowTopLevel')
try {
return (
await gitRevParseShowTopLevel(directory)
)
} catch (e) {
if (e instanceof Error) handleCommandError(e)
}
}
/**
* @param {string} directory
* @param {string} registry
* @param {boolean} force
* @param {string} message
* @param {string | { name: string; email: string }} author
*/
async function iterate (directory, registry, force, message, author) {
log('iterate')
if (directory) {
const D = resolve(directory)
try {
if (D === await gitRevParseShowTopLevel(D)) {
const B = await isHeadDefaultBranch(D)
if (B) {
const ignore = await getIgnoreFromConfiguration(D)
if (!ignore) {
await execute(D, registry, force, await toMessage(message, D), await toAuthor(author, D))
}
}
}
} catch (e) {
if (e instanceof Error) handleCommandError(e)
}
}
}
/**
* @param {string} directory
* @param {string} registry
* @param {boolean} force
* @param {string} message
* @param {string | { name: string; email: string }} author
*/
async function iteratePath (directory, registry, force, message, author) {
log('iteratePath')
if (directory) {
const D = resolve(directory)
const pathList = await getPathList(D)
if (pathList.length) {
const depsList = await getDepsList(pathList)
if (depsList.length) {
for (const d of genDirsList(depsList)) if (d) await iterate(d, registry, force, message, author)
}
}
}
}
/**
* @param {string} directory
* @param {string} registry
* @param {boolean} force
* @param {string} message
* @param {string | { name: string; email: string }} author
*/
async function execute (directory = DIRECTORY, registry = REGISTRY, force = false, message = MESSAGE, author = AUTHOR) {
log('execute')
try {
info(`Directory is "${directory}"`)
info(`Registry is "${registry}"`)
info(`Force is "${force}"`)
info(`Message is "${message}"`)
info(`Author is "${author}"`)
await gitPull(directory)
await rmrf(directory)
await npmi(directory, registry, force)
await deps(directory, registry, force)
await gitAdd(directory)
await gitCommit(directory, message, author)
await gitPush(directory)
await gitPushTags(directory)
} catch (e) {
if (e instanceof Error) handleError(e)
}
}
/**
* @param {string} directory
* @param {string} registry
* @param {boolean} force
* @param {string} message
* @param {string | { name: string; email: string }} author
*/
async function executeFrom (directory, registry, force, message, author) {
log('executeFrom')
if (directory) {
const D = resolve(directory)
try {
if (D === await gitRevParseShowTopLevel(D)) {
const B = await isHeadDefaultBranch(D)
if (B) {
const ignore = await getIgnoreFromConfiguration(D)
if (!ignore) {
await execute(D, registry, force, await toMessage(message, D), await toAuthor(author, D))
}
}
}
} catch (e) {
if (e instanceof Error) handleCommandError(e)
}
}
}
/**
* @param {string} directory
* @param {string} registry
* @param {boolean} force
* @param {string} message
* @param {string | { name: string; email: string }} author
*/
async function executeOnly (directory, registry, force, message, author) {
log('executeOnly')
if (directory) {
const D = resolve(directory)
try {
if (D === await gitRevParseShowTopLevel(D)) {
const B = await isHeadDefaultBranch(D)
if (B) {
const ignore = await getIgnoreFromConfiguration(D)
if (!ignore) {
await execute(D, registry, force, await toMessage(message, D), await toAuthor(author, D))
}
}
}
} catch (e) {
if (e instanceof Error) handleCommandError(e)
}
}
}
/**
* @param {string} directory
* @param {string} registry
* @param {boolean} force
* @param {string} message
* @param {string | { name: string; email: string }} author
*/
async function executePath (directory, registry, force, message, author) {
log('executePath')
if (directory) {
const D = resolve(directory)
try {
if (D === await gitRevParseShowTopLevel(D)) {
const B = await isHeadDefaultBranch(D)
if (B) {
const ignore = await getIgnoreFromConfiguration(D)
if (!ignore) {
await execute(D, registry, force, await toMessage(message, D), await toAuthor(author, D))
}
}
}
} catch (e) {
const { // @ts-expect-error
code
} = e
if (code === 128) {
return (
await iteratePath(directory, registry, force, message, author)
)
}
if (e instanceof Error) handleCommandError(e)
}
}
}
async function app () {
log('Deps')
const commander = new Command()
const {
argv
} = process
/*
* `version` is printed into this file at pre-commit
*/
commander
.version(VERSION)
.option('-p, --path [path]', 'Update path', '.')
.option('-f, --from [from]', 'Update from directory')
.option('-o, --only [only]', 'Update only directory')
.option('--registry [registry]', 'Installation registry')
.option('--force [force]', 'Force installation`', false)
.option('-m, --message [message]', 'Git commit message')
.option('-a, --author [author]', 'Git commit author')
.parse(argv)
const {
path: P,
from: F,
only: O,
registry,
force,
message,
author
} = commander.opts()
log({
...(P ? { path: P } : {}),
...(F ? { from: F } : {}),
...(O ? { only: O } : {}),
...(registry ? { registry } : {}),
...(force ? { force } : {}),
...(message ? { message } : {}),
...(author ? { author } : {})
})
if (P || (!F && !O)) {
log('Path')
try {
await executePath(P, registry, force, message, author)
} catch (e) {
if (e instanceof Error) handleError(e)
}
} else {
if (F) {
log('From')
try {
await executeFrom(P, registry, force, message, author)
} catch (e) {
if (e instanceof Error) handleError(e)
}
} else {
if (O) {
log('Only')
try {
await executeOnly(P, registry, force, message, author)
} catch (e) {
if (e instanceof Error) handleError(e)
}
}
}
}
log('Done.')
}
export default app()