webapps/scripts/manage.mjs
Dominik Milacher 0dc24c4db7
Some checks failed
Build and deploy updated apps / Build & deploy (push) Failing after 50s
Extend ux layer and overhaul panoramablick-saalbach.at
2025-11-21 21:17:52 +01:00

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',
},
]
}