867 lines
24 KiB
TypeScript
867 lines
24 KiB
TypeScript
import * as plugins from "./mod.plugins.js";
|
|
|
|
export interface IInstalledPackage {
|
|
name: string;
|
|
version: string;
|
|
globalDir?: string;
|
|
packagePath?: string;
|
|
legacy?: boolean;
|
|
}
|
|
|
|
export interface IPackageUpdateInfo {
|
|
name: string;
|
|
currentVersion: string;
|
|
latestVersion: string;
|
|
needsUpdate: boolean;
|
|
needsMigration?: boolean;
|
|
globalDir?: string;
|
|
}
|
|
|
|
export interface IPackageManagerInfo {
|
|
available: boolean;
|
|
currentVersion: string;
|
|
latestVersion: string | null;
|
|
needsUpdate: boolean;
|
|
}
|
|
|
|
export interface ILegacyGlobalRootInfo {
|
|
globalDir: string;
|
|
packages: IInstalledPackage[];
|
|
unmanagedPackageNames: string[];
|
|
safeToDelete: boolean;
|
|
}
|
|
|
|
export interface ILegacyCleanupResult {
|
|
globalDir: string;
|
|
deleted: boolean;
|
|
reason?: string;
|
|
}
|
|
|
|
export interface IShimSyncResult {
|
|
name: string;
|
|
action: "updated" | "removed" | "skipped";
|
|
reason?: string;
|
|
}
|
|
|
|
interface IPnpmListProject {
|
|
path?: string;
|
|
dependencies?: Record<string, any>;
|
|
}
|
|
|
|
export class PackageManagerUtil {
|
|
private shell = new plugins.smartshell.Smartshell({
|
|
executor: "bash",
|
|
});
|
|
private pnpmCommand: string | null | undefined;
|
|
|
|
public async detectPnpm(): Promise<boolean> {
|
|
return Boolean(await this.getPnpmCommand());
|
|
}
|
|
|
|
public async getPnpmVersionInfo(): Promise<IPackageManagerInfo> {
|
|
const available = await this.detectPnpm();
|
|
if (!available) {
|
|
return {
|
|
available: false,
|
|
currentVersion: "unknown",
|
|
latestVersion: null,
|
|
needsUpdate: false,
|
|
};
|
|
}
|
|
|
|
const currentVersion = await this.getCurrentPnpmVersion();
|
|
const latestVersion = await this.getLatestVersion("pnpm", [
|
|
"https://registry.npmjs.org",
|
|
]);
|
|
|
|
return {
|
|
available: true,
|
|
currentVersion,
|
|
latestVersion,
|
|
needsUpdate: latestVersion
|
|
? this.isNewerVersion(currentVersion, latestVersion)
|
|
: false,
|
|
};
|
|
}
|
|
|
|
public async getInstalledPackages(): Promise<IInstalledPackage[]> {
|
|
const packageMap = new Map<string, IInstalledPackage>();
|
|
const currentPackages = await this.getCurrentInstalledPackages();
|
|
|
|
for (const packageInfo of currentPackages) {
|
|
packageMap.set(packageInfo.name, packageInfo);
|
|
}
|
|
|
|
const legacyRoots = await this.getLegacyGlobalRoots();
|
|
for (const legacyRoot of legacyRoots) {
|
|
for (const packageInfo of legacyRoot.packages) {
|
|
if (!packageMap.has(packageInfo.name)) {
|
|
packageMap.set(packageInfo.name, packageInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
return Array.from(packageMap.values()).sort((packageA, packageB) =>
|
|
packageA.name.localeCompare(packageB.name),
|
|
);
|
|
}
|
|
|
|
public async getLegacyGlobalRoots(): Promise<ILegacyGlobalRootInfo[]> {
|
|
const currentGlobalDir = await this.getCurrentGlobalDir();
|
|
const baseDirs = new Set<string>();
|
|
const pnpmHome = process.env.PNPM_HOME;
|
|
|
|
if (pnpmHome) {
|
|
baseDirs.add(plugins.path.join(pnpmHome, "global"));
|
|
}
|
|
|
|
if (currentGlobalDir) {
|
|
baseDirs.add(plugins.path.dirname(currentGlobalDir));
|
|
}
|
|
|
|
const roots: ILegacyGlobalRootInfo[] = [];
|
|
for (const baseDir of baseDirs) {
|
|
try {
|
|
const entries = await plugins.fs.readdir(baseDir, {
|
|
withFileTypes: true,
|
|
});
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory()) {
|
|
continue;
|
|
}
|
|
|
|
const globalDir = normalizePath(
|
|
plugins.path.join(baseDir, entry.name),
|
|
);
|
|
if (currentGlobalDir && pathsAreEqual(globalDir, currentGlobalDir)) {
|
|
continue;
|
|
}
|
|
|
|
const rootInfo = await this.inspectGlobalRoot(globalDir, false);
|
|
if (rootInfo.packages.length > 0) {
|
|
roots.push(rootInfo);
|
|
}
|
|
}
|
|
} catch {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return roots;
|
|
}
|
|
|
|
public async cleanupLegacyGlobalRoots(): Promise<ILegacyCleanupResult[]> {
|
|
const legacyRoots = await this.getLegacyGlobalRoots();
|
|
if (legacyRoots.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const currentPackageNames = new Set(
|
|
(await this.getCurrentInstalledPackages()).map(
|
|
(packageInfo) => packageInfo.name,
|
|
),
|
|
);
|
|
const cleanupResults: ILegacyCleanupResult[] = [];
|
|
|
|
for (const legacyRoot of legacyRoots) {
|
|
const missingPackageNames = legacyRoot.packages
|
|
.map((packageInfo) => packageInfo.name)
|
|
.filter((packageName) => !currentPackageNames.has(packageName));
|
|
|
|
if (missingPackageNames.length > 0) {
|
|
cleanupResults.push({
|
|
globalDir: legacyRoot.globalDir,
|
|
deleted: false,
|
|
reason: `kept because ${missingPackageNames.join(", ")} are not installed in the current pnpm global root`,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (!legacyRoot.safeToDelete) {
|
|
cleanupResults.push({
|
|
globalDir: legacyRoot.globalDir,
|
|
deleted: false,
|
|
reason:
|
|
legacyRoot.unmanagedPackageNames.length > 0
|
|
? `kept because it also contains ${legacyRoot.unmanagedPackageNames.join(", ")}`
|
|
: "kept because it is not a managed @git.zone-only global root",
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const blockingShims = await this.getShimReferences(legacyRoot.globalDir);
|
|
if (blockingShims === null) {
|
|
cleanupResults.push({
|
|
globalDir: legacyRoot.globalDir,
|
|
deleted: false,
|
|
reason:
|
|
"kept because PNPM_HOME is not set, so command shims could not be verified",
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (blockingShims.length > 0) {
|
|
cleanupResults.push({
|
|
globalDir: legacyRoot.globalDir,
|
|
deleted: false,
|
|
reason: `kept because command shims still reference it: ${blockingShims.join(", ")}`,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
await plugins.fs.rm(legacyRoot.globalDir, {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
cleanupResults.push({
|
|
globalDir: legacyRoot.globalDir,
|
|
deleted: true,
|
|
});
|
|
} catch (error) {
|
|
cleanupResults.push({
|
|
globalDir: legacyRoot.globalDir,
|
|
deleted: false,
|
|
reason: `delete failed: ${(error as Error).message}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
return cleanupResults;
|
|
}
|
|
|
|
public async syncCurrentGlobalShims(): Promise<IShimSyncResult[]> {
|
|
const pnpmShimDirs = await this.getPnpmShimDirs();
|
|
if (!pnpmShimDirs) {
|
|
return [
|
|
{
|
|
name: "PNPM_HOME",
|
|
action: "skipped",
|
|
reason: "PNPM_HOME is not set",
|
|
},
|
|
];
|
|
}
|
|
|
|
const results: IShimSyncResult[] = [];
|
|
const currentBinNames = new Set<string>();
|
|
const currentPackages = await this.getCurrentInstalledPackages();
|
|
|
|
for (const packageInfo of currentPackages) {
|
|
if (!packageInfo.packagePath) {
|
|
continue;
|
|
}
|
|
|
|
const packageJson = await readJson(
|
|
plugins.path.join(packageInfo.packagePath, "package.json"),
|
|
);
|
|
const binNames = getPackageBinNames(packageInfo.name, packageJson);
|
|
const nodeModulesDir = getNodeModulesDir(
|
|
packageInfo.packagePath,
|
|
packageInfo.name,
|
|
);
|
|
|
|
for (const binName of binNames) {
|
|
currentBinNames.add(binName);
|
|
const sourceShim = plugins.path.join(nodeModulesDir, ".bin", binName);
|
|
|
|
for (const pnpmShimDir of pnpmShimDirs) {
|
|
const destinationShim = plugins.path.join(pnpmShimDir, binName);
|
|
|
|
try {
|
|
const sourceContent = await plugins.fs.readFile(sourceShim, "utf8");
|
|
const sourceStat = await plugins.fs.stat(sourceShim);
|
|
await plugins.fs.writeFile(
|
|
destinationShim,
|
|
rewriteShimForPnpmHome(sourceContent),
|
|
"utf8",
|
|
);
|
|
await plugins.fs.chmod(destinationShim, sourceStat.mode);
|
|
results.push({
|
|
name: formatShimName(pnpmShimDir, binName),
|
|
action: "updated",
|
|
});
|
|
} catch (error) {
|
|
results.push({
|
|
name: formatShimName(pnpmShimDir, binName),
|
|
action: "skipped",
|
|
reason: (error as Error).message,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const legacyRoots = await this.getLegacyGlobalRoots();
|
|
const legacyGlobalDirs = legacyRoots.map(
|
|
(legacyRoot) => legacyRoot.globalDir,
|
|
);
|
|
if (legacyGlobalDirs.length === 0) {
|
|
return results;
|
|
}
|
|
|
|
for (const pnpmShimDir of pnpmShimDirs) {
|
|
try {
|
|
const entries = await plugins.fs.readdir(pnpmShimDir, {
|
|
withFileTypes: true,
|
|
});
|
|
for (const entry of entries) {
|
|
if (!entry.isFile() || currentBinNames.has(entry.name)) {
|
|
continue;
|
|
}
|
|
|
|
const filePath = plugins.path.join(pnpmShimDir, entry.name);
|
|
let content = "";
|
|
try {
|
|
content = await plugins.fs.readFile(filePath, "utf8");
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
!legacyGlobalDirs.some((legacyGlobalDir) =>
|
|
content.includes(legacyGlobalDir),
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
await plugins.fs.rm(filePath, { force: true });
|
|
results.push({
|
|
name: formatShimName(pnpmShimDir, entry.name),
|
|
action: "removed",
|
|
});
|
|
} catch (error) {
|
|
results.push({
|
|
name: formatShimName(pnpmShimDir, entry.name),
|
|
action: "skipped",
|
|
reason: (error as Error).message,
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
results.push({
|
|
name: pnpmShimDir,
|
|
action: "skipped",
|
|
reason: (error as Error).message,
|
|
});
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
public async getLatestVersion(
|
|
packageName: string,
|
|
registries = [
|
|
"https://verdaccio.lossless.digital",
|
|
"https://registry.npmjs.org",
|
|
],
|
|
): Promise<string | null> {
|
|
for (const registry of registries) {
|
|
const latest = await this.getLatestVersionFromRegistry(
|
|
registry,
|
|
packageName,
|
|
);
|
|
if (latest) {
|
|
return latest;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public async installLatest(
|
|
packageName: string,
|
|
version = "latest",
|
|
): Promise<boolean> {
|
|
const pnpmCommand = await this.getPnpmCommand();
|
|
if (!pnpmCommand) {
|
|
return false;
|
|
}
|
|
|
|
const packageSpecifier = `${packageName}@${version}`;
|
|
console.log(` Installing ${packageSpecifier} via pnpm...`);
|
|
|
|
try {
|
|
const result = await this.shell.exec(
|
|
`${pnpmCommand} add -g ${shellQuote(packageSpecifier)}`,
|
|
);
|
|
return result.exitCode === 0;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async updatePnpm(targetVersion: string): Promise<boolean> {
|
|
const pnpmCommand = await this.getPnpmCommand();
|
|
if (!pnpmCommand) {
|
|
return false;
|
|
}
|
|
|
|
const neutralDir = process.env.PNPM_HOME || "/tmp";
|
|
|
|
try {
|
|
const result = await this.shell.exec(
|
|
`${pnpmCommand} --dir ${shellQuote(neutralDir)} self-update ${shellQuote(targetVersion)}`,
|
|
);
|
|
this.pnpmCommand = undefined;
|
|
const currentVersion = await this.getCurrentPnpmVersion();
|
|
return (
|
|
result.exitCode === 0 &&
|
|
!this.isNewerVersion(currentVersion, targetVersion)
|
|
);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public isNewerVersion(current: string, latest: string): boolean {
|
|
const currentParts = normalizeSemver(current);
|
|
const latestParts = normalizeSemver(latest);
|
|
|
|
for (
|
|
let i = 0;
|
|
i < Math.max(currentParts.length, latestParts.length);
|
|
i++
|
|
) {
|
|
const currentPart = currentParts[i] || 0;
|
|
const latestPart = latestParts[i] || 0;
|
|
if (latestPart > currentPart) return true;
|
|
if (latestPart < currentPart) return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async getCurrentPnpmVersion(): Promise<string> {
|
|
try {
|
|
const result = await this.execPnpmSilent("--version 2>/dev/null");
|
|
if (!result) {
|
|
return "unknown";
|
|
}
|
|
const versionMatch = result.stdout.trim().match(/(\d+\.\d+\.\d+)/);
|
|
return versionMatch?.[1] || "unknown";
|
|
} catch {
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
private async getPnpmCommand(): Promise<string | null> {
|
|
if (this.pnpmCommand !== undefined) {
|
|
return this.pnpmCommand;
|
|
}
|
|
|
|
const candidates = [
|
|
"pnpm --pm-on-fail=ignore",
|
|
"pnpm --config.manage-package-manager-versions=false",
|
|
"pnpm",
|
|
];
|
|
|
|
for (const candidate of candidates) {
|
|
try {
|
|
const result = await this.shell.execSilent(
|
|
`${candidate} --version 2>/dev/null`,
|
|
);
|
|
if (result.exitCode === 0 && Boolean(result.stdout.trim())) {
|
|
this.pnpmCommand = candidate;
|
|
return this.pnpmCommand;
|
|
}
|
|
} catch {
|
|
// Try the next supported pnpm invocation form.
|
|
}
|
|
}
|
|
|
|
this.pnpmCommand = null;
|
|
return this.pnpmCommand;
|
|
}
|
|
|
|
private async execPnpmSilent(commandArgs: string): Promise<any | null> {
|
|
const pnpmCommand = await this.getPnpmCommand();
|
|
if (!pnpmCommand) {
|
|
return null;
|
|
}
|
|
return this.shell.execSilent(`${pnpmCommand} ${commandArgs}`);
|
|
}
|
|
|
|
private async getPnpmListProjects(): Promise<IPnpmListProject[]> {
|
|
try {
|
|
const result = await this.execPnpmSilent(
|
|
"list -g --depth=0 --json 2>/dev/null || true",
|
|
);
|
|
const output = result?.stdout.trim();
|
|
if (!output) {
|
|
return [];
|
|
}
|
|
|
|
const data = JSON.parse(output);
|
|
return Array.isArray(data) ? data : [data];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private async getCurrentGlobalDir(): Promise<string | null> {
|
|
const listProjects = await this.getPnpmListProjects();
|
|
for (const listProject of listProjects) {
|
|
if (typeof listProject.path === "string" && listProject.path.length > 0) {
|
|
return normalizeGlobalDir(listProject.path);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const result = await this.execPnpmSilent("root -g 2>/dev/null");
|
|
const output = result?.stdout.trim();
|
|
return output ? normalizeGlobalDir(output) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private async getCurrentInstalledPackages(): Promise<IInstalledPackage[]> {
|
|
const listProjects = await this.getPnpmListProjects();
|
|
const currentGlobalDir = await this.getCurrentGlobalDir();
|
|
const packageMap = new Map<string, IInstalledPackage>();
|
|
|
|
for (const listProject of listProjects) {
|
|
const globalDir = listProject.path
|
|
? normalizeGlobalDir(listProject.path)
|
|
: currentGlobalDir || undefined;
|
|
const dependencies = listProject.dependencies || {};
|
|
for (const [name, info] of Object.entries(dependencies)) {
|
|
if (!name.startsWith("@git.zone/")) {
|
|
continue;
|
|
}
|
|
packageMap.set(name, {
|
|
name,
|
|
version: getDependencyVersion(info),
|
|
globalDir,
|
|
packagePath: getDependencyPackagePath(info),
|
|
legacy: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (packageMap.size === 0 && currentGlobalDir) {
|
|
const currentRootInfo = await this.inspectGlobalRoot(
|
|
currentGlobalDir,
|
|
true,
|
|
);
|
|
for (const packageInfo of currentRootInfo.packages) {
|
|
packageMap.set(packageInfo.name, packageInfo);
|
|
}
|
|
}
|
|
|
|
return Array.from(packageMap.values());
|
|
}
|
|
|
|
private async inspectGlobalRoot(
|
|
globalDir: string,
|
|
current: boolean,
|
|
): Promise<ILegacyGlobalRootInfo> {
|
|
const normalizedGlobalDir = normalizeGlobalDir(globalDir);
|
|
const rootPackageJson = await readJson(
|
|
plugins.path.join(normalizedGlobalDir, "package.json"),
|
|
);
|
|
const dependencies = getDependencyMap(rootPackageJson?.dependencies);
|
|
const packageMap = new Map<string, IInstalledPackage>();
|
|
|
|
for (const [name, spec] of Object.entries(dependencies)) {
|
|
if (!name.startsWith("@git.zone/")) {
|
|
continue;
|
|
}
|
|
packageMap.set(name, {
|
|
name,
|
|
version:
|
|
(await this.getInstalledPackageVersion(normalizedGlobalDir, name)) ||
|
|
normalizeDependencySpec(spec),
|
|
globalDir: normalizedGlobalDir,
|
|
packagePath: getPackagePath(normalizedGlobalDir, name),
|
|
legacy: !current,
|
|
});
|
|
}
|
|
|
|
const gitZoneScopeDir = plugins.path.join(
|
|
normalizedGlobalDir,
|
|
"node_modules",
|
|
"@git.zone",
|
|
);
|
|
try {
|
|
const entries = await plugins.fs.readdir(gitZoneScopeDir, {
|
|
withFileTypes: true,
|
|
});
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
continue;
|
|
}
|
|
const name = `@git.zone/${entry.name}`;
|
|
packageMap.set(name, {
|
|
name,
|
|
version:
|
|
(await this.getInstalledPackageVersion(
|
|
normalizedGlobalDir,
|
|
name,
|
|
)) || "unknown",
|
|
globalDir: normalizedGlobalDir,
|
|
packagePath: getPackagePath(normalizedGlobalDir, name),
|
|
legacy: !current,
|
|
});
|
|
}
|
|
} catch {
|
|
// A pnpm global root may be empty or may not have node_modules materialized yet.
|
|
}
|
|
|
|
const dependencyNames = Object.keys(dependencies);
|
|
const unmanagedPackageNames = dependencyNames.filter(
|
|
(packageName) => !packageName.startsWith("@git.zone/"),
|
|
);
|
|
|
|
return {
|
|
globalDir: normalizedGlobalDir,
|
|
packages: Array.from(packageMap.values()).sort((packageA, packageB) =>
|
|
packageA.name.localeCompare(packageB.name),
|
|
),
|
|
unmanagedPackageNames,
|
|
safeToDelete:
|
|
!current &&
|
|
dependencyNames.length > 0 &&
|
|
unmanagedPackageNames.length === 0,
|
|
};
|
|
}
|
|
|
|
private async getInstalledPackageVersion(
|
|
globalDir: string,
|
|
packageName: string,
|
|
): Promise<string | null> {
|
|
const packageJson = await readJson(
|
|
plugins.path.join(
|
|
globalDir,
|
|
"node_modules",
|
|
...packageName.split("/"),
|
|
"package.json",
|
|
),
|
|
);
|
|
return typeof packageJson?.version === "string"
|
|
? packageJson.version
|
|
: null;
|
|
}
|
|
|
|
private async getShimReferences(
|
|
legacyGlobalDir: string,
|
|
): Promise<string[] | null> {
|
|
const pnpmShimDirs = await this.getPnpmShimDirs();
|
|
if (!pnpmShimDirs) {
|
|
return null;
|
|
}
|
|
|
|
const references: string[] = [];
|
|
for (const pnpmShimDir of pnpmShimDirs) {
|
|
try {
|
|
const entries = await plugins.fs.readdir(pnpmShimDir, {
|
|
withFileTypes: true,
|
|
});
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.isFile()) {
|
|
continue;
|
|
}
|
|
|
|
const filePath = plugins.path.join(pnpmShimDir, entry.name);
|
|
try {
|
|
const content = await plugins.fs.readFile(filePath, "utf8");
|
|
if (content.includes(legacyGlobalDir)) {
|
|
references.push(formatShimName(pnpmShimDir, entry.name));
|
|
}
|
|
} catch {
|
|
// Ignore unreadable or non-text files in PNPM_HOME.
|
|
}
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return references;
|
|
}
|
|
|
|
private async getPnpmShimDirs(): Promise<string[] | null> {
|
|
const pnpmHome = process.env.PNPM_HOME;
|
|
if (!pnpmHome) {
|
|
return null;
|
|
}
|
|
|
|
const candidateDirs = [plugins.path.join(pnpmHome, "bin"), pnpmHome];
|
|
const shimDirs: string[] = [];
|
|
for (const candidateDir of candidateDirs) {
|
|
try {
|
|
const stat = await plugins.fs.stat(candidateDir);
|
|
if (stat.isDirectory()) {
|
|
shimDirs.push(normalizePath(candidateDir));
|
|
}
|
|
} catch {
|
|
// Ignore missing pnpm shim directories.
|
|
}
|
|
}
|
|
|
|
return shimDirs.length > 0 ? Array.from(new Set(shimDirs)) : null;
|
|
}
|
|
|
|
private async getLatestVersionFromRegistry(
|
|
registry: string,
|
|
packageName: string,
|
|
): Promise<string | null> {
|
|
const encodedName = packageName.replace("/", "%2f");
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 8000);
|
|
|
|
try {
|
|
const response = await fetch(`${registry}/${encodedName}`, {
|
|
signal: controller.signal,
|
|
headers: {
|
|
accept: "application/json",
|
|
},
|
|
});
|
|
if (!response.ok) {
|
|
return null;
|
|
}
|
|
const data = await response.json();
|
|
const latest = (data as any)["dist-tags"]?.latest;
|
|
return typeof latest === "string" && latest.length > 0 ? latest : null;
|
|
} catch {
|
|
return null;
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function readJson(filePath: string): Promise<any | null> {
|
|
try {
|
|
const content = await plugins.fs.readFile(filePath, "utf8");
|
|
return JSON.parse(content);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getDependencyMap(value: unknown): Record<string, string> {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return {};
|
|
}
|
|
return value as Record<string, string>;
|
|
}
|
|
|
|
function getDependencyVersion(info: any): string {
|
|
if (info && typeof info === "object" && typeof info.version === "string") {
|
|
return info.version;
|
|
}
|
|
if (typeof info === "string") {
|
|
return normalizeDependencySpec(info);
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
function getDependencyPackagePath(info: any): string | undefined {
|
|
return info && typeof info === "object" && typeof info.path === "string"
|
|
? normalizePath(info.path)
|
|
: undefined;
|
|
}
|
|
|
|
function getPackagePath(globalDir: string, packageName: string): string {
|
|
return plugins.path.join(
|
|
globalDir,
|
|
"node_modules",
|
|
...packageName.split("/"),
|
|
);
|
|
}
|
|
|
|
function getPackageBinNames(packageName: string, packageJson: any): string[] {
|
|
const bin = packageJson?.bin;
|
|
if (typeof bin === "string") {
|
|
return [getDefaultBinName(packageName)];
|
|
}
|
|
if (!bin || typeof bin !== "object" || Array.isArray(bin)) {
|
|
return [];
|
|
}
|
|
return Object.keys(bin)
|
|
.filter((binName) => binName.length > 0)
|
|
.sort();
|
|
}
|
|
|
|
function getDefaultBinName(packageName: string): string {
|
|
const packageNameParts = packageName.split("/");
|
|
return packageNameParts[packageNameParts.length - 1] || packageName;
|
|
}
|
|
|
|
function getNodeModulesDir(packagePath: string, packageName: string): string {
|
|
return packageName.startsWith("@")
|
|
? plugins.path.dirname(plugins.path.dirname(packagePath))
|
|
: plugins.path.dirname(packagePath);
|
|
}
|
|
|
|
function rewriteShimForPnpmHome(content: string): string {
|
|
const targetMatch = content.match(/^# cmd-shim-target=(.+)$/m);
|
|
if (!targetMatch?.[1]) {
|
|
return content;
|
|
}
|
|
|
|
const absoluteTarget = `"${escapeDoubleQuotedShell(targetMatch[1])}"`;
|
|
return content.replace(
|
|
/"\$basedir\/(?:\.\.\/)+store\/[^"\n]+"/g,
|
|
absoluteTarget,
|
|
);
|
|
}
|
|
|
|
function escapeDoubleQuotedShell(value: string): string {
|
|
return value.replace(/["\\$`]/g, "\\$&");
|
|
}
|
|
|
|
function formatShimName(pnpmShimDir: string, binName: string): string {
|
|
const pnpmHome = process.env.PNPM_HOME;
|
|
if (!pnpmHome) {
|
|
return binName;
|
|
}
|
|
|
|
const relativeName = plugins.path.relative(
|
|
pnpmHome,
|
|
plugins.path.join(pnpmShimDir, binName),
|
|
);
|
|
return relativeName && !relativeName.startsWith("..")
|
|
? relativeName
|
|
: binName;
|
|
}
|
|
|
|
function normalizeDependencySpec(spec: unknown): string {
|
|
if (typeof spec !== "string" || spec.length === 0) {
|
|
return "unknown";
|
|
}
|
|
const versionMatch = spec.match(/\d+\.\d+\.\d+(?:[-+][\w.-]+)?/);
|
|
return versionMatch?.[0] || spec;
|
|
}
|
|
|
|
function normalizeGlobalDir(globalDir: string): string {
|
|
const normalizedPath = normalizePath(globalDir);
|
|
return plugins.path.basename(normalizedPath) === "node_modules"
|
|
? plugins.path.dirname(normalizedPath)
|
|
: normalizedPath;
|
|
}
|
|
|
|
function normalizePath(filePath: string): string {
|
|
return plugins.path.resolve(filePath).replace(/\/+$/, "");
|
|
}
|
|
|
|
function pathsAreEqual(pathA: string, pathB: string): boolean {
|
|
return normalizePath(pathA) === normalizePath(pathB);
|
|
}
|
|
|
|
function normalizeSemver(version: string): number[] {
|
|
return version
|
|
.replace(/^[^\d]*/, "")
|
|
.split(".")
|
|
.map((part) => parseInt(part, 10) || 0);
|
|
}
|
|
|
|
function shellQuote(value: string): string {
|
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
}
|