Files
cloudflare--workers-sdk/packages/workers-utils/src/package-resolution.ts
T
wehub-resource-sync 70bf21e064
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:11 +08:00

147 lines
4.3 KiB
TypeScript

import { statSync } from "node:fs";
import path from "node:path";
import { parsePackageJSON, readFileSync } from "./parse";
/**
* Resolves the filesystem path for an installed npm package.
*
* Tries two strategies:
* 1. `require.resolve("<pkg>/package.json")` -- works when the package exports its package.json
* 2. `require.resolve("<pkg>")` -- fallback for packages that don't export package.json
*
* @param packageName - The npm package name to resolve
* @param projectPath - The project directory to resolve from
* @returns The resolved directory path, or `undefined` if the package is not installed
*/
export function getPackagePath(
packageName: string,
projectPath: string
): string | undefined {
try {
// Try to resolve the package.json directly — works when the package exports it
return path.dirname(
require.resolve(`${packageName}/package.json`, {
paths: [projectPath],
})
);
} catch {}
try {
// Fallback: resolve the package entry point and return its directory
return path.dirname(
require.resolve(packageName, {
paths: [projectPath],
})
);
} catch {}
return undefined;
}
/**
* Checks whether an npm package is installed in a target project.
*
* @param packageName - The name of the target package
* @param projectPath - The path of the project to check
* @returns `true` if the package is installed, `false` otherwise
*/
export function isPackageInstalled(
packageName: string,
projectPath: string
): boolean {
return !!getPackagePath(packageName, projectPath);
}
/**
* Gets the exact version of an npm package installed in a project by resolving
* it from node_modules and reading its package.json.
*
* @param packageName - The name of the target package
* @param projectPath - The path of the project to check
* @param opts - Options
* @param opts.stopAtProjectPath - If `true`, stop walking up at the project's path
* @returns The installed version string, or `undefined` if the package is not installed
*/
export function getInstalledPackageVersion(
packageName: string,
projectPath: string,
opts: {
stopAtProjectPath?: boolean;
} = {}
): string | undefined {
try {
const packagePath = getPackagePath(packageName, projectPath);
if (!packagePath) {
return undefined;
}
const lastDir = opts.stopAtProjectPath === true ? projectPath : undefined;
const packageJsonPath = findFileUp("package.json", packagePath, lastDir);
if (!packageJsonPath) {
return undefined;
}
const packageJson = parsePackageJSON(
readFileSync(packageJsonPath),
packageJsonPath
);
// The requested package may be installed under an alias (e.g. vite+
// installs `@voidzero-dev/vite-plus-core` under the `vite` alias). In that
// case the resolved package.json belongs to the aliased package, so its
// `version` is not the version of the requested package.
//
// `bundledVersions` is NOT a standard package.json field (it is not the
// standard `bundledDependencies`) — it is a vite+ convention that maps the
// names of the tools it bundles to the versions it provides. When the
// resolved package name doesn't match the requested one, prefer the version
// declared there for the requested package.
if (packageJson.name !== packageName) {
const bundledVersion = packageJson.bundledVersions?.[packageName];
if (bundledVersion !== undefined) {
return bundledVersion;
}
}
return packageJson.version;
} catch {}
}
/**
* Walks up from `startDir` looking for a file named `name`.
* Stops at `lastDir` (inclusive) if provided, otherwise walks to the filesystem root.
*
* @param name - The filename to search for
* @param startDir - The directory to start searching from
* @param lastDir - If provided, stop searching after reaching this directory
* @returns The full path to the found file, or `undefined`
*/
function findFileUp(
name: string,
startDir: string,
lastDir?: string
): string | undefined {
let dir = startDir;
const root = path.parse(dir).root;
while (true) {
const candidate = path.join(dir, name);
try {
if (statSync(candidate).isFile()) {
return candidate;
}
} catch {}
if (lastDir !== undefined && dir === lastDir) {
break;
}
const parent = path.dirname(dir);
if (parent === dir || dir === root) {
break;
}
dir = parent;
}
return undefined;
}