Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add getPackageInfo utility function #50

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,8 @@
"packageManager": "[email protected]",
"engines": {
"node": "^14.16.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"dependencies": {
"ofetch": "^1.0.1"
}
}
9 changes: 5 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 16 additions & 5 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
import { ofetch } from 'ofetch'
import { detectPackageManager } from "./detect";
import { runCorepack } from "./spawn";
import { PackageManagerName } from "./types";
import { Package, PackageManager } from "./types";

export interface APICallOptions {
cwd: string;
packageManager: PackageManagerName;
packageManager: PackageManager;
silent: boolean;
}

export interface AddDependencyOptions extends Partial<APICallOptions> { dev?: boolean; }
export async function addDependency (name: string, _options: AddDependencyOptions = {}) {
const options = await _resolveOptions(_options);
const command = options.packageManager === "npm" ? "install" : "add";
const command = options.packageManager.name === "npm" ? "install" : "add";
const argv = [options.dev ? "-D" : ""].filter(Boolean);
await runCorepack(options.packageManager, [command, ...argv, name], { cwd: options.cwd, silent: options.silent });
await runCorepack(options.packageManager.command, [command, ...argv, name], { cwd: options.cwd, silent: options.silent });
return {};
}

export async function getPackageInfo (name: string): Promise<Package> {
const pkg = await ofetch(`https://registry.npmjs.org/${name}`, {
onResponseError({ request, response }) {
console.error('Could not fetch package: ' + name + "\n", request, response.status)
}
});

return pkg;
}

async function _resolveOptions<T extends APICallOptions> (options: Partial<T> = {}): Promise<T> {
options.cwd = options.cwd || process.cwd();
options.silent = options.silent ?? (process.env.NODE_ENV === "test");
if (!options.packageManager) {
const detected = await detectPackageManager(options.cwd);
options.packageManager = detected?.name || "npm";
options.packageManager = detected;
}
return options as T;
}
76 changes: 58 additions & 18 deletions src/detect.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,87 @@
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { join, normalize } from "pathe";
import type { PackageManagerName } from "./types";
import type { PackageManager } from "./types";

const packageManagerLocks: Record<string, PackageManagerName> = {
"yarn.lock": "yarn",
"package-lock.json": "npm",
"pnpm-lock.yaml": "pnpm"
};
const packageManagers: PackageManager[] = [
{ name: "npm", command: "npm", lockFile: "package-lock.json" },
{
name: "pnpm",
command: "pnpm",
lockFile: "pnpm-lock.yaml",
files: ["pnpm-workspace.yaml"],
},
{
name: "yarn",
command: "yarn",
majorVersion: "1.0.0",
lockFile: "yarn.lock",
},
{
name: "yarn",
command: "yarn",
majorVersion: "2.0.0",
lockFile: "yarn.lock",
files: [".yarnrc.yml"],
},
];

export interface DetectPackageManagerOptions {
ignoreLockFile?: boolean;
ignorePackageJSON?: boolean;
}

export async function detectPackageManager (cwd: string, options: DetectPackageManagerOptions = {}): Promise<{ name: PackageManagerName, version?: string }> {
export async function detectPackageManager(
cwd: string,
options: DetectPackageManagerOptions = {}
): Promise<PackageManager | undefined> {
const detected = await findup(cwd, async (path) => {
// 1. Use `packageManager` field from package.json
if (!options.ignorePackageJSON) {
const packageJSONPath = join(path, "package.json");
if (existsSync(packageJSONPath)) {
const packageJSON = JSON.parse(await readFile(packageJSONPath, "utf8"));
if (packageJSON?.packageManager) {
const [name, version] = packageJSON.packageManager.split("@");
return { name, version };
const [name, version = "0.0.0"] =
packageJSON.packageManager.split("@");
const majorVersion = version.split(".")[0];
const packageManager =
packageManagers.find(
(pm) => pm.name === name && pm.majorVersion === majorVersion
) || packageManagers.find((pm) => pm.name === name);
return {
...packageManager,
name,
command: name,
version,
majorVersion,
};
}
}
}
// 2. Use implicit file detection
if (!options.ignoreLockFile) {
for (const lockFile in packageManagerLocks) {
if (existsSync(join(path, lockFile))) {
return { name: packageManagerLocks[lockFile] };
for (const packageManager of packageManagers) {
const detectionsFiles = [
packageManager.lockFile,
...(packageManager.files || []),
].filter(Boolean) as string[];
if (detectionsFiles.some((file) => existsSync(resolve(path, file)))) {
return {
...packageManager,
};
}
}
}
});
return {
name: "npm",
version: "latest", // TODO
...detected
};
return detected;
}

async function findup<T> (cwd: string, match: (path: string) => T | Promise<T>): Promise<T | undefined> {
async function findup<T>(
cwd: string,
match: (path: string) => T | Promise<T>
): Promise<T | undefined> {
const segments = normalize(cwd).split("/");
while (segments.length > 0) {
const path = segments.join("/");
Expand Down
31 changes: 30 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
export type PackageManagerName = "npm" | "yarn" | "pnpm"
export type PackageManagerName = "npm" | "yarn" | "pnpm";

export interface PackageManager {
name: PackageManagerName;
command: string;
version?: string;
majorVersion?: string;
lockFile?: string;
files?: string[];
}

export interface PackageMaintainer {
name: string;
email: string;
}

export interface Package {
_id: string;
_rev: string;
time: Record<string, string>;
name: string;
versions: Record<string, Record<string, unknown>>;
maintainers: PackageMaintainer[]
readme: string;
readmeFilename: string;
description: string;
homepage: string;
repository: string;
license: string;
}
28 changes: 14 additions & 14 deletions test/fixtures/npm/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions test/fixtures/npm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"private": true,
"version": "0.0.0",
"dependencies": {
"pathe": "^1.0.0"
"pathe": "^1.1.0"
},
"packageManager": "[email protected]",
"devDependencies": {
"ufo": "^1.0.1"
"ufo": "^1.1.1"
}
}
2 changes: 1 addition & 1 deletion test/fixtures/pnpm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dependencies": {
"pathe": "^1.0.0"
},
"packageManager": "pnpm@7.18.0",
"packageManager": "pnpm@8.0.0",
"devDependencies": {
"ufo": "^1.0.1"
}
Expand Down
18 changes: 9 additions & 9 deletions test/fixtures/pnpm/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"private": true,
"version": "0.0.0",
"dependencies": {
"pathe": "^1.0.0"
"pathe": "^1.1.0"
},
"packageManager": "[email protected]",
"devDependencies": {
"ufo": "^1.0.1"
"ufo": "^1.1.1"
}
}
13 changes: 13 additions & 0 deletions test/fixtures/yarn-berry/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


pathe@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.0.tgz#e2e13f6c62b31a3289af4ba19886c230f295ec03"
integrity sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==

ufo@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.1.1.tgz#e70265e7152f3aba425bd013d150b2cdf4056d7c"
integrity sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==
12 changes: 12 additions & 0 deletions test/fixtures/yarn-classic/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "fixture-yarn",
"private": true,
"version": "0.0.0",
"dependencies": {
"pathe": "^1.1.0"
},
"packageManager": "[email protected]",
"devDependencies": {
"ufo": "^1.1.1"
}
}
13 changes: 13 additions & 0 deletions test/fixtures/yarn-classic/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


pathe@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.0.tgz#e2e13f6c62b31a3289af4ba19886c230f295ec03"
integrity sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==

ufo@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.1.1.tgz#e70265e7152f3aba425bd013d150b2cdf4056d7c"
integrity sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==
Loading