From 583569e8f5a16b64e8e1ca23e224f9092ad4712d Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 22 Sep 2021 20:38:11 -0400 Subject: [PATCH] yarn pnp compat: copy binary into the current pkg --- CHANGELOG.md | 4 ++-- lib/npm/node-install.ts | 4 ++-- lib/npm/node-platform.ts | 29 +++++------------------------ scripts/esbuild.js | 5 ++++- 4 files changed, 13 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b43e9089b49..b6858b18ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,11 @@ * Fix the `esbuild` package in yarn 2+ - The [yarn package manager](https://yarnpkg.com/) version 2 and above has a mode called [PnP](https://next.yarnpkg.com/features/pnp/) that installs packages inside zip files instead of using individual files on disk, and then hijacks node's `fs` module to pretend that paths to files inside the zip file are actually individual files on disk so that code that wasn't written specifically for yarn. Unfortunately that hijacking is incomplete and it still causes certain things to break such as using these zip file paths to create a JavaScript worker thread or to create a child process. + The [yarn package manager](https://yarnpkg.com/) version 2 and above has a mode called [PnP](https://next.yarnpkg.com/features/pnp/) that installs packages inside zip files instead of using individual files on disk, and then hijacks node's `fs` module to pretend that paths to files inside the zip file are actually individual files on disk so that code that wasn't written specifically for yarn still works. Unfortunately that hijacking is incomplete and it still causes certain things to break such as using these zip file paths to create a JavaScript worker thread or to create a child process. This was an issue for the new `optionalDependencies` package installation strategy that was just released in version 0.13.0 since the binary executable is now inside of an installed package instead of being downloaded using an install script. When it's installed with yarn 2+ in PnP mode the binary executable is inside a zip file and can't be run. To work around this, esbuild detects yarn's PnP mode and copies the binary executable to a real file outside of the zip file. - Unfortunately the code to do this didn't create the parent directory before writing to the file path. That caused esbuild's API to crash when it was run for the first time. This didn't come up during testing because the parent directory already existed when the tests were run. This release adds code to create the parent directory first, which should fix this crash. This problem only affected esbuild's JS API when it was run through yarn 2+ with PnP mode active. + Unfortunately the code to do this didn't create the parent directory before writing to the file path. That caused esbuild's API to crash when it was run for the first time. This didn't come up during testing because the parent directory already existed when the tests were run. This release changes the location of the binary executable from a shared cache directory to inside the esbuild package itself, which should fix this crash. This problem only affected esbuild's JS API when it was run through yarn 2+ with PnP mode active. ## 0.13.0 diff --git a/lib/npm/node-install.ts b/lib/npm/node-install.ts index 1da5fe7dd3a..83bdb4c4db1 100644 --- a/lib/npm/node-install.ts +++ b/lib/npm/node-install.ts @@ -1,4 +1,4 @@ -import { pkgAndBinForCurrentPlatform } from './node-platform'; +import { binPathForCurrentPlatform } from './node-platform'; import fs = require('fs'); import os = require('os'); @@ -59,7 +59,7 @@ if (process.env.ESBUILD_BINARY_PATH) { // doesn't apply when npm's "--ignore-scripts" flag is used since in that case // this install script will not be run. else if (os.platform() !== 'win32' && !isYarn2OrAbove()) { - const { bin } = pkgAndBinForCurrentPlatform(); + const bin = binPathForCurrentPlatform(); try { fs.unlinkSync(toPath); fs.linkSync(bin, toPath); diff --git a/lib/npm/node-platform.ts b/lib/npm/node-platform.ts index f0801a47a60..8d829c83ca9 100644 --- a/lib/npm/node-platform.ts +++ b/lib/npm/node-platform.ts @@ -2,8 +2,6 @@ import fs = require('fs'); import os = require('os'); import path = require('path'); -declare const ESBUILD_VERSION: string; - // This feature was added to give external code a way to modify the binary // path without modifying the code itself. Do not remove this because // external code relies on this. @@ -31,7 +29,7 @@ export const knownUnixlikePackages: Record = { 'sunos x64 LE': 'esbuild-sunos-64', }; -export function pkgAndBinForCurrentPlatform(): { pkg: string, bin: string } { +export function binPathForCurrentPlatform(): string { let pkg: string; let bin: string; let platformKey = `${process.platform} ${os.arch()} ${os.endianness()}`; @@ -65,21 +63,7 @@ by esbuild to install the correct binary executable for your current platform.`) throw e } - return { pkg, bin }; -} - -function getCachePath(name: string): string { - const home = os.homedir(); - const common = ['esbuild', 'bin', `${name}@${ESBUILD_VERSION}`]; - if (process.platform === 'darwin') return path.join(home, 'Library', 'Caches', ...common); - if (process.platform === 'win32') return path.join(home, 'AppData', 'Local', 'Cache', ...common); - - // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html - const XDG_CACHE_HOME = process.env.XDG_CACHE_HOME; - if (process.platform === 'linux' && XDG_CACHE_HOME && path.isAbsolute(XDG_CACHE_HOME)) - return path.join(XDG_CACHE_HOME, ...common); - - return path.join(home, '.cache', ...common); + return bin; } export function extractedBinPath(): string { @@ -90,7 +74,7 @@ export function extractedBinPath(): string { return ESBUILD_BINARY_PATH; } - const { pkg, bin } = pkgAndBinForCurrentPlatform(); + const bin = binPathForCurrentPlatform(); // The esbuild binary executable can't be used in Yarn 2 in PnP mode because // it's inside a virtual file system and the OS needs it in the real file @@ -103,12 +87,9 @@ export function extractedBinPath(): string { } catch (e) { } if (isYarnPnP) { - const binTargetPath = getCachePath(pkg); + const esbuildLibDir = path.dirname(require.resolve('esbuild')); + const binTargetPath = path.join(esbuildLibDir, 'yarn-pnp-' + path.basename(bin)); if (!fs.existsSync(binTargetPath)) { - fs.mkdirSync(path.dirname(binTargetPath), { - recursive: true, - mode: 0o700, // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html - }); fs.copyFileSync(bin, binTargetPath); fs.chmodSync(binTargetPath, 0o755); } diff --git a/scripts/esbuild.js b/scripts/esbuild.js index 7869a56c0ab..f247dac036b 100644 --- a/scripts/esbuild.js +++ b/scripts/esbuild.js @@ -25,6 +25,7 @@ exports.buildNativeLib = (esbuildPath) => { '--bundle', '--target=' + nodeTarget, '--define:ESBUILD_VERSION=' + JSON.stringify(version), + '--external:esbuild', '--platform=node', '--log-level=warning', ], { cwd: repoDir }) @@ -37,6 +38,7 @@ exports.buildNativeLib = (esbuildPath) => { '--target=' + nodeTarget, '--define:WASM=false', '--define:ESBUILD_VERSION=' + JSON.stringify(version), + '--external:esbuild', '--platform=node', '--log-level=warning', ], { cwd: repoDir }) @@ -47,7 +49,7 @@ exports.buildNativeLib = (esbuildPath) => { '--outfile=' + path.join(binDir, 'esbuild'), '--bundle', '--target=' + nodeTarget, - '--define:ESBUILD_VERSION=' + JSON.stringify(version), + '--external:esbuild', '--platform=node', '--log-level=warning', ], { cwd: repoDir }) @@ -62,6 +64,7 @@ exports.buildNativeLib = (esbuildPath) => { path.join(repoDir, 'lib', 'npm', 'node-platform.ts'), '--bundle', '--target=' + nodeTarget, + '--external:esbuild', '--platform=node', '--log-level=warning', ], { cwd: repoDir }))(platforms, require)