From ae90c02284c20bd44467034a675546eec35f589d Mon Sep 17 00:00:00 2001 From: JounQin Date: Thu, 21 Dec 2023 20:59:02 +0800 Subject: [PATCH] feat: support yarn v2+ w/wo corepack enabled fix #272 fix #496 close #506 --- src/getPackageResolution.ts | 31 +++++++++++++++++++++--------- src/makePatch.ts | 38 +++++++++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/getPackageResolution.ts b/src/getPackageResolution.ts index a046e7f9..6ab36f88 100644 --- a/src/getPackageResolution.ts +++ b/src/getPackageResolution.ts @@ -30,7 +30,14 @@ export function getPackageResolution({ throw new Error("Can't find yarn.lock file") } const lockFileString = readFileSync(lockFilePath).toString() - let appLockFile + let appLockFile: Record< + string, + { + version: string + resolution?: string + resolved?: string + } + > if (lockFileString.includes("yarn lockfile v1")) { const parsedYarnLockFile = parseYarnLockFile(lockFileString) if (parsedYarnLockFile.type !== "success") { @@ -59,7 +66,6 @@ export function getPackageResolution({ ) const resolutions = entries.map(([_, v]) => { - // @ts-ignore return v.resolved }) @@ -71,7 +77,7 @@ export function getPackageResolution({ if (new Set(resolutions).size !== 1) { console.log( - `Ambigious lockfile entries for ${packageDetails.pathSpecifier}. Using version ${installedVersion}`, + `Ambiguous lockfile entries for ${packageDetails.pathSpecifier}. Using version ${installedVersion}`, ) return installedVersion } @@ -80,18 +86,25 @@ export function getPackageResolution({ return resolutions[0] } - const resolution = entries[0][0].slice(packageDetails.name.length + 1) + const packageName = packageDetails.name + + const resolutionVersion = entries[0][1].version + + // `@backstage/integration@npm:^1.5.0, @backstage/integration@npm:^1.7.0, @backstage/integration@npm:^1.7.2` + // -> + // `^1.5.0 ^1.7.0 ^1.7.2` + const resolution = entries[0][0] + .replace(new RegExp(packageName + "@", "g"), "") + .replace(/npm:/g, "") + .replace(/,/g, "") // resolve relative file path if (resolution.startsWith("file:.")) { return `file:${resolve(appPath, resolution.slice("file:".length))}` } - if (resolution.startsWith("npm:")) { - return resolution.replace("npm:", "") - } - - return resolution + // add `resolutionVersion` to ensure correct version, `^1.0.0` could resolve latest `v1.3.0`, but `^1.0.0 1.2.1` won't + return resolutionVersion ? resolution + " " + resolutionVersion : resolution } else { const lockfile = require(join( appPath, diff --git a/src/makePatch.ts b/src/makePatch.ts index 4d040297..8fbbc24a 100644 --- a/src/makePatch.ts +++ b/src/makePatch.ts @@ -172,6 +172,8 @@ export function makePatch({ writeFileSync( tmpRepoPackageJsonPath, JSON.stringify({ + // support `corepack` enabled without `.yarn/releases` + packageManager: appPackageJson.packageManager, dependencies: { [packageDetails.name]: getPackageResolution({ packageDetails, @@ -193,7 +195,14 @@ export function makePatch({ // copy .npmrc/.yarnrc in case packages are hosted in private registry // copy .yarn directory as well to ensure installations work in yarn 2 // tslint:disable-next-line:align - ;[".npmrc", ".yarnrc", ".yarn"].forEach((rcFile) => { + ;[ + ".npmrc", + ".yarnrc", + ".yarnrc.yml", + // don't include the whole `.yarn` directory which could contain huge `cache` + ".yarn/plugins", + ".yarn/releases", + ].forEach((rcFile) => { const rcPath = join(appPath, rcFile) if (existsSync(rcPath)) { copySync(rcPath, join(tmpRepo.name, rcFile), { dereference: true }) @@ -205,10 +214,19 @@ export function makePatch({ chalk.grey("•"), `Installing ${packageDetails.name}@${packageVersion} with yarn`, ) + const yarnArgs = ["install"] + const yarnVersionCmd = spawnSafeSync(`yarn`, ["--version"], { + cwd: tmpRepoNpmRoot, + logStdErrOnError: false, + }) + const isYarnV1 = yarnVersionCmd.stdout.toString().startsWith("1.") + if (isYarnV1) { + yarnArgs.push("--ignore-engines") + } try { // try first without ignoring scripts in case they are required // this works in 99.99% of cases - spawnSafeSync(`yarn`, ["install", "--ignore-engines"], { + spawnSafeSync(`yarn`, yarnArgs, { cwd: tmpRepoNpmRoot, logStdErrOnError: false, }) @@ -217,7 +235,7 @@ export function makePatch({ // an implicit context which we haven't reproduced spawnSafeSync( `yarn`, - ["install", "--ignore-engines", "--ignore-scripts"], + [...yarnArgs, isYarnV1 ? "--ignore-scripts" : "--mode=skip-build"], { cwd: tmpRepoNpmRoot, }, @@ -338,9 +356,8 @@ export function makePatch({ try { parsePatchFile(diffResult.stdout.toString()) } catch (e) { - if ( - (e as Error).message.includes("Unexpected file mode string: 120000") - ) { + const err = e as Error + if (err.message.includes("Unexpected file mode string: 120000")) { console.log(` ⛔️ ${chalk.red.bold("ERROR")} @@ -358,7 +375,7 @@ export function makePatch({ outPath, gzipSync( JSON.stringify({ - error: { message: e.message, stack: e.stack }, + error: { message: err.message, stack: err.stack }, patch: diffResult.stdout.toString(), }), ), @@ -544,7 +561,12 @@ export function makePatch({ } } } catch (e) { - console.log(e) + const err = e as Error & { + stdout?: Buffer + stderr?: Buffer + } + // try to log more useful error message + console.log(err.stderr?.toString() || err.stdout?.toString() || e) throw e } finally { tmpRepo.removeCallback()