diff --git a/src/_utils.ts b/src/_utils.ts index 2ca297d..d2f19b0 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -2,7 +2,11 @@ import { createRequire } from "node:module"; import { normalize, resolve } from "pathe"; import { withTrailingSlash } from "ufo"; import { x } from "tinyexec"; -import type { OperationOptions, PackageManager } from "./types"; +import type { + OperationOptions, + PackageManager, + PackageManagerName, +} from "./types"; import type { DetectPackageManagerOptions } from "./package-manager"; import { detectPackageManager, packageManagers } from "./package-manager"; @@ -158,3 +162,32 @@ export function doesDependencyExist( return false; } } + +export function parsePackageManagerField(packageManager?: string): { + name?: PackageManagerName; + version?: string; + buildMeta?: string; + warnings?: string[]; +} { + const [name, _version] = (packageManager || "").split("@"); + const [version, buildMeta] = _version?.split("+") || []; + + if ( + name && + name !== "-" && + /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name) + ) { + return { name: name as PackageManagerName, version, buildMeta }; + } + + const sanitized = name.replace(/\W+/g, ""); + const warnings = [ + `Abnormal characters found in \`packageManager\` field, sanitizing from \`${name}\` to \`${sanitized}\``, + ]; + return { + name: sanitized as PackageManagerName, + version, + buildMeta, + warnings, + }; +} diff --git a/src/cli.ts b/src/cli.ts index 8a11d29..4418d88 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -89,10 +89,18 @@ const detect = defineCommand({ run: async ({ args }) => { const cwd = resolve(args.cwd || "."); const packageManager = await detectPackageManager(cwd); + + if (packageManager?.warnings) { + for (const warning of packageManager.warnings) { + consola.warn(warning); + } + } + if (!packageManager) { consola.error(`Cannot detect package manager in \`${cwd}\``); return process.exit(1); } + consola.log( `Detected package manager in \`${cwd}\`: \`${packageManager.name}@${packageManager.version}\``, ); diff --git a/src/package-manager.ts b/src/package-manager.ts index 95a9860..b6238fa 100644 --- a/src/package-manager.ts +++ b/src/package-manager.ts @@ -1,7 +1,7 @@ import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { join, resolve } from "pathe"; -import { findup } from "./_utils"; +import { findup, parsePackageManagerField } from "./_utils"; import type { PackageManager } from "./types"; export type DetectPackageManagerOptions = { @@ -80,7 +80,7 @@ export const packageManagers: PackageManager[] = [ export async function detectPackageManager( cwd: string, options: DetectPackageManagerOptions = {}, -): Promise { +): Promise<(PackageManager & { warnings?: string[] }) | undefined> { const detected = await findup( resolve(cwd || "."), async (path) => { @@ -92,20 +92,28 @@ export async function detectPackageManager( await readFile(packageJSONPath, "utf8"), ); if (packageJSON?.packageManager) { - 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, + const { name, - command: name, - version, - majorVersion, - }; + version = "0.0.0", + buildMeta, + warnings, + } = parsePackageManagerField(packageJSON.packageManager); + if (name) { + const majorVersion = version.split(".")[0]; + const packageManager = + packageManagers.find( + (pm) => pm.name === name && pm.majorVersion === majorVersion, + ) || packageManagers.find((pm) => pm.name === name); + return { + name, + command: name, + version, + majorVersion, + buildMeta, + warnings, + ...packageManager, + }; + } } } diff --git a/src/types.ts b/src/types.ts index c088cb1..f4634a0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,7 @@ export type PackageManager = { name: PackageManagerName; command: string; version?: string; + buildMeta?: string; majorVersion?: string; lockFile?: string | string[]; files?: string[]; diff --git a/test/_utils.test.ts b/test/_utils.test.ts index 7836cff..95eeb5d 100644 --- a/test/_utils.test.ts +++ b/test/_utils.test.ts @@ -1,5 +1,8 @@ import { expect, it, describe } from "vitest"; -import { resolveOperationOptions } from "../src/_utils"; +import { + parsePackageManagerField, + resolveOperationOptions, +} from "../src/_utils"; describe("internal utils", () => { describe("resolveOperationOptions", () => { @@ -8,4 +11,35 @@ describe("internal utils", () => { expect(r.packageManager.name).toBe("yarn"); }); }); + + describe("parsePackageManagerField", () => { + const cases = { + "": [""], + "-": [""], + "*": [""], + "^npm": ["npm"], + npm: ["npm"], + "unknown-name": ["unknown-name"], + unknownName: ["unknownName"], + "npm@1.2.3": ["npm", "1.2.3"], + "pnpm@9.15.4+sha512.b2dc20e2fc72b3e": [ + "pnpm", + "9.15.4", + "sha512.b2dc20e2fc72b3e", + ], + }; + for (const [input, [name, version, buildMeta]] of Object.entries(cases)) { + it(input, () => { + const p = parsePackageManagerField(input); + expect(p.name).toBe(name); + expect(p.version).toBe(version); + expect(p.buildMeta).toBe(buildMeta); + if (input.split("@")[0] !== name) { + expect(p.warnings).toMatchObject([ + `Abnormal characters found in \`packageManager\` field, sanitizing from \`${input}\` to \`${name}\``, + ]); + } + }); + } + }); });