Skip to content

Commit

Permalink
yarn pnp compat: copy binary into the current pkg
Browse files Browse the repository at this point in the history
  • Loading branch information
John Doe committed Sep 23, 2021
1 parent 98b0640 commit 583569e
Show file tree
Hide file tree
Showing 4 changed files with 13 additions and 29 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions lib/npm/node-install.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { pkgAndBinForCurrentPlatform } from './node-platform';
import { binPathForCurrentPlatform } from './node-platform';

import fs = require('fs');
import os = require('os');
Expand Down Expand Up @@ -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);
Expand Down
29 changes: 5 additions & 24 deletions lib/npm/node-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -31,7 +29,7 @@ export const knownUnixlikePackages: Record<string, string> = {
'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()}`;
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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);
}
Expand Down
5 changes: 4 additions & 1 deletion scripts/esbuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand All @@ -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 })
Expand All @@ -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 })
Expand All @@ -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)
Expand Down

0 comments on commit 583569e

Please sign in to comment.