Some checks failed
Build and deploy updated apps / Build & deploy (push) Failing after 50s
387 lines
11 KiB
JavaScript
387 lines
11 KiB
JavaScript
import { run } from './auxilary.mjs'
|
|
import { readFileSync, writeFileSync } from 'node:fs'
|
|
import { join, resolve } from 'path'
|
|
import { select, input } from '@inquirer/prompts'
|
|
import { cwd } from 'node:process'
|
|
import chalk from 'chalk'
|
|
|
|
const invocationDir = resolve(process.env.INIT_CWD)
|
|
const workspaceDir = resolve(cwd())
|
|
|
|
const action = await select({
|
|
message: 'What do you want to do?',
|
|
choices: [
|
|
{
|
|
name: 'List dependencies',
|
|
value: 'list',
|
|
description: 'List all direct dependencies',
|
|
},
|
|
{
|
|
name: 'Check dependencies',
|
|
value: 'check',
|
|
description: 'Check for version inconsistencies',
|
|
},
|
|
{
|
|
name: 'Fix dependencies',
|
|
value: 'fix',
|
|
description: 'Resolve inconsistencies for a dependency',
|
|
},
|
|
{
|
|
name: 'Add dependency',
|
|
value: 'add',
|
|
description: 'Add a new dependency',
|
|
},
|
|
{
|
|
name: 'Remove dependency',
|
|
value: 'remove',
|
|
description: 'Remove a dependency',
|
|
},
|
|
{
|
|
name: 'Change dependency type',
|
|
value: 'change',
|
|
description: 'Change the type of a dependency',
|
|
},
|
|
],
|
|
})
|
|
|
|
const roots = findPackageRoots()
|
|
const jsons = readPackageJsons(roots)
|
|
const summary = summarizePackageJsons(jsons)
|
|
const problems = identifyProblems(jsons, summary)
|
|
|
|
if (action === 'list') {
|
|
const good = sortMap(filterMap(summary, ([n]) => !problems.has(n)))
|
|
const bad = sortMap(filterMap(summary, ([n]) => problems.has(n)))
|
|
|
|
if (good.size) {
|
|
console.log(chalk.green(`\n${good.size} good dependencies:`))
|
|
|
|
for (const [name, versions] of good) {
|
|
const [version, users] = versions.entries().next().value
|
|
|
|
const packages = [...users]
|
|
.filter(([r, ts]) => !(r === workspaceDir && ts.length === 1 && ts[0] === 'overrides'))
|
|
.map(([r]) => jsons.get(r).name)
|
|
|
|
console.log(` ${chalk.green(name)}@${version}: ${packages.length}x (${packages.join(', ')})`)
|
|
}
|
|
}
|
|
|
|
if (bad.size) {
|
|
console.log(chalk.red(`\n${bad.size} bad dependencies:`))
|
|
printProblems(problems, ' ')
|
|
}
|
|
} else if (action === 'check') {
|
|
if (problems.size) {
|
|
printProblems(problems)
|
|
process.exit(1)
|
|
}
|
|
} else if (action === 'fix') {
|
|
for (const [name, ps] of problems) {
|
|
console.log(`Fixing ${name}:`)
|
|
|
|
for (const p of ps) {
|
|
process.stdout.write(` ${p.title}: `)
|
|
|
|
if (p.fix) {
|
|
p.fix()
|
|
console.log(chalk.green('Fixed'))
|
|
} else {
|
|
console.log(chalk.red('Skipped'))
|
|
}
|
|
}
|
|
}
|
|
} else if (action === 'add') {
|
|
const name = (await input({ message: `Name of the package:` })).trim()
|
|
let version
|
|
|
|
//if (roots.map(r => jsons.get(r).name))
|
|
|
|
if ([...jsons].map(([r, j]) => j.name).includes(name)) {
|
|
version = 'workspace:*'
|
|
console.log('Using version workspace:* for local package')
|
|
} else if (summary.get(name)?.size === 1) {
|
|
version = [...summary.get(name)][0][0]
|
|
console.log(`Reusing currently used version ${version}`)
|
|
} else {
|
|
version = (await input({ message: `Version of the package:` })).trim()
|
|
}
|
|
|
|
const type = await select({
|
|
message: 'Which type is the dependency?',
|
|
choices: getDependencyTypeChoices(),
|
|
})
|
|
|
|
const flag = invocationDir === workspaceDir ? '-w' : ''
|
|
const identifier = version ? `${name}@${version}` : name
|
|
|
|
run(`cd ${invocationDir} && pnpm add ${flag} ${type} ${identifier}`)
|
|
|
|
const packageSummary = summarizePackageJson(readPackageJson(invocationDir))
|
|
const versions = packageSummary.get(name)
|
|
|
|
if ((versions?.size ?? 2) > 1) {
|
|
console.error(
|
|
`Zero or more than 1 versions specified in ${join(invocationDir, 'package.json')} after pnpm add`
|
|
)
|
|
process.exit(2)
|
|
}
|
|
|
|
const workspaceJson = readPackageJson(workspaceDir)
|
|
updatePackageJsonValue(workspaceJson, `pnpm.overrides.${name}`, [...versions][0][0])
|
|
writePackageJson(workspaceDir, workspaceJson)
|
|
run(`cd ${workspaceDir} && pnpm install --no-frozen-lockfile`)
|
|
} else if (action === 'remove') {
|
|
const name = await select({
|
|
message: 'Which dependency do you want to remove?',
|
|
choices: getDependencyChoices(),
|
|
})
|
|
|
|
const flag = invocationDir === workspaceDir ? '-w' : ''
|
|
run(`cd ${invocationDir} && pnpm remove ${flag} ${name}`)
|
|
|
|
const newRoots = findPackageRoots()
|
|
const newJsons = readPackageJsons(newRoots)
|
|
const newSummary = summarizePackageJsons(newJsons)
|
|
const newProblems = identifyProblems(newJsons, newSummary).get(name) ?? []
|
|
|
|
const overrideRedundant = newProblems.find((p) => p.id === 'override-redundant')
|
|
overrideRedundant?.fix()
|
|
} else if (action === 'change') {
|
|
const name = await select({
|
|
message: 'Which dependency do you want to change?',
|
|
choices: getDependencyChoices(),
|
|
})
|
|
|
|
const type = await select({
|
|
message: 'What should the new dependency type be?',
|
|
choices: getDependencyTypeChoices(),
|
|
})
|
|
|
|
const flag = invocationDir === workspaceDir ? '-w' : ''
|
|
run(`cd ${invocationDir} && pnpm install ${flag} ${type} ${name}`)
|
|
|
|
if (type !== '--save-peer') {
|
|
const newJson = readPackageJson(invocationDir)
|
|
updatePackageJsonValue(newJson, `peerDependencies.${name}`, undefined)
|
|
writePackageJson(invocationDir, newJson) // no need to run pnpm install afterwards for peer dependencies
|
|
}
|
|
}
|
|
|
|
function findPackageRoots() {
|
|
const output = run('pnpm list --recursive --depth -1 --json')
|
|
return JSON.parse(output).map((p) => resolve(p.path))
|
|
}
|
|
|
|
function readPackageJson(root) {
|
|
return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
}
|
|
|
|
function readPackageJsons(roots) {
|
|
return new Map(roots.map((r) => [r, readPackageJson(r)]))
|
|
}
|
|
|
|
function writePackageJson(root, json) {
|
|
writeFileSync(join(root, 'package.json'), JSON.stringify(json, null, 2), 'utf8')
|
|
}
|
|
|
|
function updatePackageJsonValue(json, name, value) {
|
|
const parents = name.split('.')
|
|
const target = parents.pop()
|
|
|
|
for (const parent of parents) {
|
|
if (!(parent in json)) {
|
|
if (value === undefined) return
|
|
json[parent] = {}
|
|
}
|
|
|
|
json = json[parent]
|
|
}
|
|
|
|
if (value === undefined && json !== undefined) {
|
|
delete json[target]
|
|
} else if (value !== undefined) {
|
|
json[target] = value
|
|
}
|
|
}
|
|
|
|
function summarizePackageJson(json) {
|
|
const summary = new Map()
|
|
|
|
const getters = {
|
|
dependencies: (p) => p.dependencies,
|
|
devDependencies: (p) => p.devDependencies,
|
|
peerDependencies: (p) => p.peerDependencies,
|
|
overrides: (p) => p.pnpm?.overrides,
|
|
}
|
|
|
|
for (const [type, getter] of Object.entries(getters)) {
|
|
for (const [name, version] of Object.entries(getter(json) ?? {})) {
|
|
const outer = summary.get(name) ?? new Map()
|
|
const inner = outer.get(version) ?? []
|
|
|
|
inner.push(type)
|
|
outer.set(version, inner)
|
|
summary.set(name, outer)
|
|
}
|
|
}
|
|
|
|
return summary
|
|
}
|
|
|
|
function summarizePackageJsons(roots) {
|
|
const summary = new Map()
|
|
|
|
for (const [root, json] of roots) {
|
|
for (const [name, versions] of summarizePackageJson(json)) {
|
|
for (const [version, types] of versions) {
|
|
const outer = summary.get(name) ?? new Map()
|
|
const inner = outer.get(version) ?? new Map()
|
|
|
|
inner.set(root, types)
|
|
outer.set(version, inner)
|
|
summary.set(name, outer)
|
|
}
|
|
}
|
|
}
|
|
|
|
return summary
|
|
}
|
|
|
|
// TODO: all versions should be exact, app should have parent peer deps as deps (how to find parents?)
|
|
function identifyProblems(roots, summary) {
|
|
const problems = new Map()
|
|
|
|
function summarize(users) {
|
|
return [...users].map(([r, ts]) => `${roots.get(r).name}: (${ts.join(', ')})`).join(', ')
|
|
}
|
|
|
|
for (const [name, versions] of summary.entries()) {
|
|
if (versions.size > 1) {
|
|
const list = problems.get(name) ?? []
|
|
|
|
list.push({
|
|
id: 'version-mismatch',
|
|
name: chalk.red(name),
|
|
title: 'Version mismatch detected',
|
|
lines: [...versions].map(([v, us]) => `${chalk.red(v)}: ${summarize(us)}`),
|
|
})
|
|
|
|
problems.set(name, list)
|
|
}
|
|
|
|
for (const [version, users] of versions.entries()) {
|
|
const workspace = roots.get(workspaceDir).name
|
|
const list = problems.get(name) ?? []
|
|
|
|
for (const [root, types] of users) {
|
|
if (types.includes('peerDependencies') && !types.includes('devDependencies')) {
|
|
list.push({
|
|
id: 'peer-missing-dev',
|
|
name: chalk.red(name),
|
|
title: 'Peer dependency missing development dependency',
|
|
lines: [
|
|
`Entry ${name}@${version} required in devDependencies for ${roots.get(root).name}`,
|
|
],
|
|
fix: () => {
|
|
const flag = invocationDir === workspaceDir ? '-w' : ''
|
|
const identifier = version ? `${name}@${version}` : name
|
|
|
|
run(`cd ${invocationDir} && pnpm add ${flag} --save-peer ${identifier}`)
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
const override = [...users].find(([u, ts]) => workspaceDir === u && ts.includes('overrides'))
|
|
|
|
if (!override) {
|
|
list.push({
|
|
id: 'override-missing',
|
|
name: chalk.red(name),
|
|
title: 'No override specified',
|
|
lines: [`Entry ${name}@${version} required in pnpm.overrides for ${workspace}`],
|
|
fix: () => {
|
|
const json = roots.get(workspaceDir)
|
|
updatePackageJsonValue(json, `pnpm.overrides.${name}`, version)
|
|
writePackageJson(workspaceDir, json)
|
|
run(`cd ${workspaceDir} && pnpm install --no-frozen-lockfile`)
|
|
},
|
|
})
|
|
} else if (users.size === 1 && [...users][0][1].length === 1) {
|
|
list.push({
|
|
id: 'override-redundant',
|
|
name: chalk.red(name),
|
|
title: 'Override specified without use',
|
|
lines: [`Entry ${name}@${version} in pnpm.overrides for ${workspace} but never used`],
|
|
fix: () => {
|
|
const json = roots.get(workspaceDir)
|
|
updatePackageJsonValue(json, `pnpm.overrides.${name}`, undefined)
|
|
writePackageJson(workspaceDir, json)
|
|
run(`cd ${workspaceDir} && pnpm install --no-frozen-lockfile`)
|
|
},
|
|
})
|
|
}
|
|
|
|
if (list.length) {
|
|
problems.set(name, list)
|
|
}
|
|
}
|
|
}
|
|
|
|
return problems
|
|
}
|
|
|
|
function printProblems(problems, indent = '') {
|
|
for (const [name, versions] of sortMap(problems)) {
|
|
console.log(`${indent}${chalk.red(name)}:`)
|
|
for (const problem of problems.get(name)) {
|
|
const lines = [`${indent} ${chalk.red(problem.title)}`, ...problem.lines]
|
|
console.log(lines.join(`\n${indent} `))
|
|
}
|
|
}
|
|
}
|
|
|
|
function filterMap(map, predicate) {
|
|
return new Map([...map].filter(predicate))
|
|
}
|
|
|
|
function sortMap(map) {
|
|
return new Map([...map].sort(([a], [b]) => a.localeCompare(b)))
|
|
}
|
|
|
|
function getDependencyChoices() {
|
|
const json = readPackageJson(invocationDir)
|
|
const summary = summarizePackageJson(json)
|
|
|
|
const choices = [...summary]
|
|
.filter(([n, vs]) => {
|
|
const types = new Set([...vs].flatMap(([v, ts]) => ts))
|
|
return types.size > 1 || types.values().next().value !== 'overrides'
|
|
})
|
|
.map(([n, vs]) => ({ name: n, value: n, description: `The ${n} package` }))
|
|
|
|
choices.sort((a, b) => a.name.localeCompare(b.name))
|
|
return choices
|
|
}
|
|
|
|
function getDependencyTypeChoices() {
|
|
return [
|
|
{
|
|
name: 'Production',
|
|
value: '--save-prod',
|
|
description: 'Required in production',
|
|
},
|
|
{
|
|
name: 'Development',
|
|
value: '--save-dev',
|
|
description: 'For development only',
|
|
},
|
|
{
|
|
name: 'Peer',
|
|
value: '--save-peer',
|
|
description: 'A peer dependency',
|
|
},
|
|
]
|
|
}
|