Files
2026-07-13 12:38:36 +08:00

151 lines
4.7 KiB
TypeScript

import { readJsonFile } from '@nx/devkit';
import { execSync } from 'child_process';
import { readJsonSync } from 'fs-extra';
import { dirname, join } from 'path';
import { globSync } from 'tinyglobby';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { copyBuiltPackage, WORKSPACE_ROOT } from './copy';
import { DirectoryPicker } from './directory-picker';
import { asciiBlock, centerStringInWidth, termSize } from './ui';
const packages: { npmName: string; nxName: string; pkgRoot: string }[] =
globSync(`packages/*/package.json`).map((p) => {
const pkgJson = readJsonSync(p);
const pkgRoot = dirname(p);
const projectJson = (() => {
try {
return readJsonFile(join(WORKSPACE_ROOT, pkgRoot, 'project.json'));
} catch {}
})();
return {
npmName: pkgJson.name,
nxName: projectJson?.name ?? pkgJson.nx?.name ?? pkgJson.name,
pkgRoot,
};
});
/**
* Score a package name against the user's input for ranking.
* Lower score = better match. Returns Infinity for no match.
*
* Matching is done against the "short name" — the part after the scope
* (e.g. "react" for "@nx/react") as well as the full name.
*
* 0 exact match on short name ("nx" → "nx")
* 1 exact match on full name
* 2 short name starts with input ("react" matches "@nx/react-native")
* 3 full name starts with input
* 4 short name contains input
* 5 full name contains input
*/
function packageMatchScore(input: string, name: string): number {
const lower = input.toLowerCase();
const fullLower = name.toLowerCase();
const shortName = fullLower.includes('/')
? fullLower.split('/').pop()!
: fullLower;
if (shortName === lower) return 0;
if (fullLower === lower) return 1;
if (shortName.startsWith(lower)) return 2;
if (fullLower.startsWith(lower)) return 3;
if (shortName.includes(lower)) return 4;
if (fullLower.includes(lower)) return 5;
return Infinity;
}
async function promptPackages(): Promise<typeof packages> {
const { prompt } = require('enquirer');
// Reserve lines for the prompt header, input, footer hint, and breathing room
const visibleChoices = Math.max(5, termSize().rows - 4);
const result: { packages: string[] } = await prompt({
type: 'autocomplete',
name: 'packages',
message: 'Select packages to copy',
choices: packages.map((p) => p.nxName),
multiple: true,
limit: visibleChoices,
suggest(input: string, choices: { name: string; message: string }[]) {
if (!input) return choices;
return choices
.map((ch) => ({ ch, score: packageMatchScore(input, ch.message) }))
.filter(({ score }) => score < Infinity)
.sort((a, b) => a.score - b.score)
.map(({ ch }) => ch);
},
});
if (result.packages.length === 0) {
console.error('No packages selected.');
process.exit(1);
}
return result.packages.map(
(nxName) => packages.find((p) => p.nxName === nxName)!
);
}
yargs(hideBin(process.argv))
.command({
command: '$0',
builder: (yargs) =>
yargs
.option('package', {
type: 'string',
choices: packages.flatMap((p) => [p.npmName, p.nxName]),
description: 'The package to copy the build outputs from',
})
.option('repo', {
type: 'string',
description:
'The root path of the repo to copy the built packages to',
})
.option('build', {
type: 'boolean',
description: 'Set to false to skip prebuild step.',
default: true,
}),
handler: async (argv) => {
const argvPackage =
argv.package &&
packages.find(
(p) => p.nxName === argv.package || p.npmName === argv.package
);
const selectedPackages = argvPackage
? [argvPackage]
: await promptPackages();
const selectedNxProjects = selectedPackages.map((p) => p.nxName);
const repo = argv.repo || (await new DirectoryPicker().run());
if (argv.build) {
execSync(
'nx run-many --tuiAutoExit=0 -t build -p ' +
selectedNxProjects.join(' '),
{
stdio: 'inherit',
}
);
}
const termWidth = process.stdout.columns || 80;
const renderWidth = termWidth - termWidth * 0.2;
for (const pkg of selectedPackages) {
console.log();
console.log(
asciiBlock(
renderWidth,
2,
1,
`Copying ${pkg.npmName} to ${repo}/node_modules/${pkg.npmName}`
)
.map((line) => centerStringInWidth(line, termWidth))
.join('\n')
);
copyBuiltPackage(pkg, repo);
}
},
})
.parseAsync();