Skip to content

Commit 5ff6e82

Browse files
authored
perf: load binaries in the same process (#97)
1 parent 876ce02 commit 5ff6e82

11 files changed

+136
-130
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
},
6161
"scripts": {
6262
"build": "rm -rf dist shims && webpack && ts-node ./mkshims.ts",
63-
"corepack": "ts-node ./sources/main.ts",
63+
"corepack": "ts-node ./sources/_entryPoint.ts",
6464
"prepack": "node ./.yarn/releases/*.*js build",
6565
"postpack": "rm -rf dist shims",
6666
"typecheck": "tsc --noEmit",

sources/Engine.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import semver from 'semver';
55

66
import defaultConfig from '../config.json';
77

8-
import * as folderUtils from './folderUtils';
98
import * as corepackUtils from './corepackUtils';
9+
import * as folderUtils from './folderUtils';
1010
import * as semverUtils from './semverUtils';
11-
import {SupportedPackageManagers, SupportedPackageManagerSet} from './types';
1211
import {Config, Descriptor, Locator} from './types';
12+
import {SupportedPackageManagers, SupportedPackageManagerSet} from './types';
1313

1414

1515
export class Engine {

sources/_entryPoint.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {runMain} from './main';
2+
3+
// Used by the generated shims
4+
export {runMain};
5+
6+
// Using `eval` to be sure that Webpack doesn't transform it
7+
if (process.mainModule === eval(`module`))
8+
runMain(process.argv.slice(2));

sources/commands/Enable.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import path from 'p
55
import which from 'which';
66

77
import {Context} from '../main';
8+
import * as nodeUtils from '../nodeUtils';
89
import {isSupportedPackageManager, SupportedPackageManagerSetWithoutNpm} from '../types';
910

1011
export class EnableCommand extends Command<Context> {
@@ -51,7 +52,7 @@ export class EnableCommand extends Command<Context> {
5152
installDirectory = fs.realpathSync(installDirectory);
5253

5354
// We use `eval` so that Webpack doesn't statically transform it.
54-
const manifestPath = eval(`require`).resolve(`corepack/package.json`);
55+
const manifestPath = nodeUtils.dynamicRequire.resolve(`corepack/package.json`);
5556

5657
const distFolder = path.join(path.dirname(manifestPath), `dist`);
5758
if (!fs.existsSync(distFolder))

sources/corepackUtils.ts

+19-78
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {StdioOptions, spawn, ChildProcess} from 'child_process';
21
import fs from 'fs';
32
import path from 'path';
43
import semver from 'semver';
@@ -7,11 +6,9 @@ import * as debugUtils from './debugUtil
76
import * as folderUtils from './folderUtils';
87
import * as fsUtils from './fsUtils';
98
import * as httpUtils from './httpUtils';
10-
import {Context} from './main';
9+
import * as nodeUtils from './nodeUtils';
1110
import {RegistrySpec, Descriptor, Locator, PackageManagerSpec} from './types';
1211

13-
declare const __non_webpack_require__: unknown;
14-
1512
export async function fetchAvailableTags(spec: RegistrySpec): Promise<Record<string, string>> {
1613
switch (spec.type) {
1714
case `npm`: {
@@ -133,7 +130,10 @@ export async function installVersion(installTarget: string, locator: Locator, {s
133130
return installFolder;
134131
}
135132

136-
export async function runVersion(installSpec: { location: string, spec: PackageManagerSpec }, locator: Locator, binName: string, args: Array<string>, context: Context) {
133+
/**
134+
* Loads the binary, taking control of the current process.
135+
*/
136+
export async function runVersion(installSpec: { location: string, spec: PackageManagerSpec }, binName: string, args: Array<string>): Promise<void> {
137137
let binPath: string | null = null;
138138
if (Array.isArray(installSpec.spec.bin)) {
139139
if (installSpec.spec.bin.some(bin => bin === binName)) {
@@ -155,82 +155,23 @@ export async function runVersion(installSpec: { location: string, spec: PackageM
155155
if (!binPath)
156156
throw new Error(`Assertion failed: Unable to locate path for bin '${binName}'`);
157157

158-
return new Promise<number>((resolve, reject) => {
159-
process.on(`SIGINT`, () => {
160-
// We don't want to exit the process before the child, so we just
161-
// ignore SIGINT and wait for the regular exit to happen (the child
162-
// will receive SIGINT too since it's part of the same process grp)
163-
});
164-
165-
const stdio: StdioOptions = [`pipe`, `pipe`, `pipe`];
166-
167-
if (context.stdin === process.stdin)
168-
stdio[0] = `inherit`;
169-
if (context.stdout === process.stdout)
170-
stdio[1] = `inherit`;
171-
if (context.stderr === process.stderr)
172-
stdio[2] = `inherit`;
173-
174-
const v8CompileCache = typeof __non_webpack_require__ !== `undefined`
175-
? eval(`require`).resolve(`./vcc.js`)
176-
: eval(`require`).resolve(`corepack/dist/vcc.js`);
177-
178-
const child = spawn(process.execPath, [`--require`, v8CompileCache, binPath!, ...args], {
179-
cwd: context.cwd,
180-
stdio,
181-
env: {
182-
...process.env,
183-
COREPACK_ROOT: path.dirname(eval(`__dirname`)),
184-
},
185-
});
186-
187-
activeChildren.add(child);
188-
189-
if (activeChildren.size === 1) {
190-
process.on(`SIGINT`, sigintHandler);
191-
process.on(`SIGTERM`, sigtermHandler);
192-
}
193-
194-
if (context.stdin !== process.stdin)
195-
context.stdin.pipe(child.stdin!);
196-
if (context.stdout !== process.stdout)
197-
child.stdout!.pipe(context.stdout);
198-
if (context.stderr !== process.stderr)
199-
child.stderr!.pipe(context.stderr);
158+
nodeUtils.registerV8CompileCache();
200159

201-
child.on(`error`, error => {
202-
activeChildren.delete(child);
160+
// We load the binary into the current process,
161+
// while making it think it was spawned.
203162

204-
if (activeChildren.size === 0) {
205-
process.off(`SIGINT`, sigintHandler);
206-
process.off(`SIGTERM`, sigtermHandler);
207-
}
163+
// Non-exhaustive list of requirements:
164+
// - Yarn uses process.argv[1] to determine its own path: https://github.com/yarnpkg/berry/blob/0da258120fc266b06f42aed67e4227e81a2a900f/packages/yarnpkg-cli/sources/main.ts#L80
165+
// - pnpm uses `require.main == null` to determine its own version: https://github.com/pnpm/pnpm/blob/e2866dee92991e979b2b0e960ddf5a74f6845d90/packages/cli-meta/src/index.ts#L14
208166

209-
reject(error);
210-
});
167+
process.env.COREPACK_ROOT = path.dirname(eval(`__dirname`));
211168

212-
child.on(`exit`, exitCode => {
213-
activeChildren.delete(child);
169+
process.argv = [
170+
process.execPath,
171+
binPath,
172+
...args,
173+
];
174+
process.execArgv = [];
214175

215-
if (activeChildren.size === 0) {
216-
process.off(`SIGINT`, sigintHandler);
217-
process.off(`SIGTERM`, sigtermHandler);
218-
}
219-
220-
resolve(exitCode !== null ? exitCode : 1);
221-
});
222-
});
223-
}
224-
225-
const activeChildren = new Set<ChildProcess>();
226-
227-
function sigintHandler() {
228-
// We don't want SIGINT to kill our process; we want it to kill the
229-
// innermost process, whose end will cause our own to exit.
230-
}
231-
232-
function sigtermHandler() {
233-
for (const child of activeChildren) {
234-
child.kill();
235-
}
176+
return nodeUtils.loadMainModule(binPath);
236177
}

sources/main.ts

+15-21
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {DisableCommand} from './command
55
import {EnableCommand} from './commands/Enable';
66
import {HydrateCommand} from './commands/Hydrate';
77
import {PrepareCommand} from './commands/Prepare';
8-
import * as miscUtils from './miscUtils';
98
import * as corepackUtils from './corepackUtils';
9+
import * as miscUtils from './miscUtils';
1010
import * as specUtils from './specUtils';
1111
import {Locator, SupportedPackageManagers, Descriptor} from './types';
1212

@@ -19,7 +19,7 @@ type PackageManagerRequest = {
1919
binaryVersion: string | null;
2020
};
2121

22-
function getPackageManagerRequestFromCli(parameter: string | undefined, context: CustomContext & Partial<Context>): PackageManagerRequest {
22+
function getPackageManagerRequestFromCli(parameter: string | undefined, context: CustomContext & Partial<Context>): PackageManagerRequest | null {
2323
if (!parameter)
2424
return null;
2525

@@ -82,14 +82,20 @@ async function executePackageManagerRequest({packageManager, binaryName, binaryV
8282
throw new UsageError(`Failed to successfully resolve '${descriptor.range}' to a valid ${descriptor.name} release`);
8383

8484
const installSpec = await context.engine.ensurePackageManager(resolved);
85-
const exitCode = await corepackUtils.runVersion(installSpec, resolved, binaryName, args, context);
8685

87-
return exitCode;
86+
return await corepackUtils.runVersion(installSpec, binaryName, args);
8887
}
8988

90-
export async function main(argv: Array<string>, context: CustomContext & Partial<Context>) {
89+
async function main(argv: Array<string>) {
9190
const corepackVersion = require(`../package.json`).version;
9291

92+
// Because we load the binaries in the same process, we don't support custom contexts.
93+
const context = {
94+
...Cli.defaultContext,
95+
cwd: process.cwd(),
96+
engine: new Engine(),
97+
};
98+
9399
const [firstArg, ...restArgs] = argv;
94100
const request = getPackageManagerRequestFromCli(firstArg, context);
95101

@@ -110,10 +116,7 @@ export async function main(argv: Array<string>, context: CustomContext & Partial
110116
cli.register(HydrateCommand);
111117
cli.register(PrepareCommand);
112118

113-
return await cli.run(argv, {
114-
...Cli.defaultContext,
115-
...context,
116-
});
119+
return await cli.run(argv, context);
117120
} else {
118121
// Otherwise, we create a single-command CLI to run the specified package manager (we still use Clipanion in order to pretty-print usage errors).
119122
const cli = new Cli({
@@ -129,25 +132,16 @@ export async function main(argv: Array<string>, context: CustomContext & Partial
129132
}
130133
});
131134

132-
return await cli.run(restArgs, {
133-
...Cli.defaultContext,
134-
...context,
135-
});
135+
return await cli.run(restArgs, context);
136136
}
137137
}
138138

139+
// Important: this is the only function that the corepack binary exports.
139140
export function runMain(argv: Array<string>) {
140-
main(argv, {
141-
cwd: process.cwd(),
142-
engine: new Engine(),
143-
}).then(exitCode => {
141+
main(argv).then(exitCode => {
144142
process.exitCode = exitCode;
145143
}, err => {
146144
console.error(err.stack);
147145
process.exitCode = 1;
148146
});
149147
}
150-
151-
// Using `eval` to be sure that Webpack doesn't transform it
152-
if (process.mainModule === eval(`module`))
153-
runMain(process.argv.slice(2));

sources/module.d.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import 'module';
2+
3+
declare module 'module' {
4+
const _cache: {[p: string]: NodeModule};
5+
6+
function _nodeModulePaths(from: string): Array<string>;
7+
function _resolveFilename(request: string, parent: NodeModule | null | undefined, isMain: boolean): string;
8+
}
9+
10+
declare global {
11+
namespace NodeJS {
12+
interface Module {
13+
load(path: string): void;
14+
}
15+
}
16+
}

sources/nodeUtils.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Module from 'module';
2+
import path from 'path';
3+
4+
declare const __non_webpack_require__: NodeRequire | undefined;
5+
6+
export const dynamicRequire: NodeRequire = typeof __non_webpack_require__ !== `undefined`
7+
? __non_webpack_require__
8+
: require;
9+
10+
function getV8CompileCachePath() {
11+
return typeof __non_webpack_require__ !== `undefined`
12+
? `./vcc.js`
13+
: `corepack/dist/vcc.js`;
14+
}
15+
16+
export function registerV8CompileCache() {
17+
const vccPath = getV8CompileCachePath();
18+
dynamicRequire(vccPath);
19+
}
20+
21+
/**
22+
* Loads a module as a main module, enabling the `require.main === module` pattern.
23+
*/
24+
export function loadMainModule(id: string): void {
25+
const modulePath = Module._resolveFilename(id, null, true);
26+
27+
const module = new Module(modulePath, undefined);
28+
29+
module.filename = modulePath;
30+
module.paths = Module._nodeModulePaths(path.dirname(modulePath));
31+
32+
Module._cache[modulePath] = module;
33+
34+
process.mainModule = module;
35+
module.id = `.`;
36+
37+
try {
38+
return module.load(modulePath);
39+
} catch (error) {
40+
delete Module._cache[modulePath];
41+
throw error;
42+
}
43+
}

tests/_runCli.ts

+25-26
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,35 @@
11
import {PortablePath, npath} from '@yarnpkg/fslib';
2-
import {PassThrough} from 'stream';
3-
4-
import {Engine} from '../sources/Engine';
5-
import {main} from '../sources/main';
2+
import {spawn} from 'child_process';
63

74
export async function runCli(cwd: PortablePath, argv: Array<string>) {
8-
const stdin = new PassThrough();
9-
const stdout = new PassThrough();
10-
const stderr = new PassThrough();
11-
125
const out: Array<Buffer> = [];
136
const err: Array<Buffer> = [];
147

15-
stdout.on(`data`, chunk => {
16-
out.push(chunk);
17-
});
8+
return new Promise((resolve, reject) => {
9+
const child = spawn(process.execPath, [require.resolve(`corepack/dist/corepack.js`), ...argv], {
10+
cwd: npath.fromPortablePath(cwd),
11+
env: process.env,
12+
stdio: `pipe`,
13+
});
1814

19-
stderr.on(`data`, chunk => {
20-
err.push(chunk);
21-
});
15+
child.stdout.on(`data`, chunk => {
16+
out.push(chunk);
17+
});
2218

23-
const exitCode = await main(argv, {
24-
cwd: npath.fromPortablePath(cwd),
25-
engine: new Engine(),
26-
stdin,
27-
stdout,
28-
stderr,
29-
});
19+
child.stderr.on(`data`, chunk => {
20+
err.push(chunk);
21+
});
22+
23+
child.on(`error`, error => {
24+
reject(error);
25+
});
3026

31-
return {
32-
exitCode,
33-
stdout: Buffer.concat(out).toString(),
34-
stderr: Buffer.concat(err).toString(),
35-
};
27+
child.on(`exit`, exitCode => {
28+
resolve({
29+
exitCode,
30+
stdout: Buffer.concat(out).toString(),
31+
stderr: Buffer.concat(err).toString(),
32+
});
33+
});
34+
});
3635
}

tsconfig.json

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
"module": "commonjs",
1212
"resolveJsonModule": true,
1313
"skipLibCheck": true,
14+
"strict": true,
1415
"target": "es2017"
16+
},
17+
"ts-node": {
18+
"transpileOnly": true
1519
}
1620
}

0 commit comments

Comments
 (0)