Skip to content

Commit

Permalink
feat(core): use corepack when enabled to sync lockfile/run npm script (
Browse files Browse the repository at this point in the history
…#775)

* feat(core): use corepack when enabled to sync lockfile/run npm script
  • Loading branch information
ghiscoding committed Nov 25, 2023
1 parent d092fc6 commit 3f5624c
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 50 deletions.
5 changes: 5 additions & 0 deletions packages/core/src/child-process.ts
Expand Up @@ -22,6 +22,7 @@ let currentColor = 0;
* @param {string} command
* @param {string[]} args
* @param {import("execa").Options} [opts]
* @param {boolean} [dryRun]
*/
export function exec(command: string, args: string[], opts?: ExecaOptions & { pkg?: Package }, dryRun = false): Promise<any> {
const options = Object.assign({ stdio: 'pipe' }, opts);
Expand All @@ -35,6 +36,7 @@ export function exec(command: string, args: string[], opts?: ExecaOptions & { pk
* @param {string} command
* @param {string[]} args
* @param {import("execa").SyncOptions} [opts]
* @param {boolean} [dryRun]
*/
export function execSync(command: string, args?: string[], opts?: ExacaSyncOptions, dryRun = false) {
// prettier-ignore
Expand All @@ -48,6 +50,7 @@ export function execSync(command: string, args?: string[], opts?: ExacaSyncOptio
* @param {string} command
* @param {string[]} args
* @param {import("execa").Options} [opts]
* @param {boolean} [dryRun]
*/
export function spawn(command: string, args: string[], opts?: ExecaOptions & { pkg?: Package }, dryRun = false): Promise<any> {
const options = Object.assign({}, opts, { stdio: 'inherit' });
Expand All @@ -62,6 +65,7 @@ export function spawn(command: string, args: string[], opts?: ExecaOptions & { p
* @param {string[]} args
* @param {import("execa").Options} [opts]
* @param {string} [prefix]
* @param {boolean} [dryRun]
*/
/* c8 ignore next */
export function spawnStreaming(
Expand Down Expand Up @@ -124,6 +128,7 @@ export function getExitCode(result: any) {
* @param {string} command
* @param {string[]} args
* @param {import("execa").Options} opts
* @param {boolean} [dryRun]
*/
export function spawnProcess(command: string, args: string[], opts: ExecaOptions & { pkg?: Package }, dryRun = false) {
if (dryRun) {
Expand Down
121 changes: 121 additions & 0 deletions packages/core/src/corepack/exec-package-manager.spec.ts
@@ -0,0 +1,121 @@
import chalk from 'chalk';
import npmlog from 'npmlog';
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';

import { execPackageManager, execPackageManagerSync } from './exec-package-manager';
import { exec, execSync, getChildProcessCount } from '../child-process';
import { Package } from '../package';

vi.mock('../child-process', async () => ({
...(await vi.importActual<any>('../child-process')),
exec: vi.fn(),
execSync: vi.fn(),
getChildProcessCount: (await vi.importActual<any>('../child-process')).getChildProcessCount,
}));

const execActual = (await vi.importActual<any>('../child-process')).exec;
const execSyncActual = (await vi.importActual<any>('../child-process')).execSync;

describe('.execPackageManagerSync()', () => {
beforeEach(() => {
process.env = {};
});

describe('mock child processes', () => {
it('calls execSync without corepack when disabled', () => {
execPackageManagerSync('echo', ['execPackageManagerSync']);

expect(execSync).toHaveBeenCalledWith('echo', ['execPackageManagerSync'], undefined, false);
});

it('calls execSync with corepack when enabled', () => {
Object.assign({}, process.env);
process.env.COREPACK_ROOT = 'pnpm';

execPackageManagerSync('echo', ['execPackageManagerSync']);

expect(execSync).toHaveBeenCalledWith('corepack', ['echo', 'execPackageManagerSync'], undefined, false);
});
});

describe('import actual child processes', () => {
beforeEach(() => {
(execSync as Mock).mockImplementationOnce(execSyncActual);
});

it('should execute a command in a child process and return the result', () => {
expect(execPackageManagerSync('echo', ['execPackageManagerSync'])).toContain(`execPackageManagerSync`);
});

it('should execute a command in dry-run and log the command', () => {
const logSpy = vi.spyOn(npmlog, 'info');
execPackageManagerSync('echo', ['execPackageManagerSync'], undefined, true);
expect(logSpy).toHaveBeenCalledWith(chalk.bold.magenta('[dry-run] >'), 'echo execPackageManagerSync');
});

it('does not error when stdout is ignored', () => {
expect(() => execPackageManagerSync('echo', ['ignored'], { stdio: 'ignore' })).not.toThrow();
});
});
});

describe('.execPackageManager()', () => {
beforeEach(() => {
process.env = {};
});

describe('mock child processes', () => {
it('calls exec without corepack when disabled', () => {
execPackageManager('echo', ['execPackageManager']);

expect(exec).toHaveBeenCalledWith('echo', ['execPackageManager'], undefined, false);
});

it('calls exec with corepack when enabled', () => {
Object.assign({}, process.env);
process.env.COREPACK_ROOT = 'pnpm';

execPackageManager('echo', ['execPackageManager']);

expect(exec).toHaveBeenCalledWith('corepack', ['echo', 'execPackageManager'], undefined, false);
});
});

describe('import actual child processes', () => {
beforeEach(() => {
(exec as Mock).mockImplementationOnce(execActual);
});

it('returns an execa Promise', async () => {
const { stderr, stdout } = (await execPackageManager('echo', ['foo'])) as any;

expect(stderr).toBe('');
expect(stdout).toContain(`foo`);
});

it('should execute a command in dry-run and log the command', () => {
const logSpy = vi.spyOn(npmlog, 'info');
execPackageManager('echo', ['exec'], undefined, true);
expect(logSpy).toHaveBeenCalledWith(chalk.bold.magenta('[dry-run] >'), 'echo exec');
});

it('rejects on undefined command', async () => {
const result = execPackageManager('nowImTheModelOfAModernMajorGeneral', undefined as any);

await expect(result).rejects.toThrow(/\bnowImTheModelOfAModernMajorGeneral\b/);
expect(getChildProcessCount()).toBe(0);
});

it('decorates opts.pkg on error if caught', async () => {
const result = execPackageManager('theVeneratedVirginianVeteranWhoseMenAreAll', ['liningUpToPutMeUpOnAPedestal'], {
pkg: { name: 'hamilton' } as Package,
});

await expect(result).rejects.toThrow(
expect.objectContaining({
pkg: { name: 'hamilton' },
})
);
});
});
});
28 changes: 28 additions & 0 deletions packages/core/src/corepack/exec-package-manager.ts
@@ -0,0 +1,28 @@
import type { Options as ExecaOptions, SyncOptions as ExacaSyncOptions } from 'execa';

import { exec, execSync } from '../child-process';
import { isCorepackEnabled } from './is-corepack-enabled';
import type { Package } from '../package';

function createCommandAndArgs(npmClient: string, args: string[]) {
let command = npmClient;
const commandArgs = args === undefined ? [] : [...args];

if (isCorepackEnabled()) {
commandArgs.unshift(command);
command = 'corepack';
}

return { command, commandArgs };
}

// prettier-ignore
export function execPackageManager(npmClient: string, args: string[], opts?: ExecaOptions & { pkg?: Package }, dryRun = false): Promise<any> {
const { command, commandArgs } = createCommandAndArgs(npmClient, args);
return exec(command, commandArgs, opts, dryRun);
}

export function execPackageManagerSync(npmClient: string, args: string[], opts?: ExacaSyncOptions, dryRun = false): string {
const { command, commandArgs } = createCommandAndArgs(npmClient, args);
return execSync(command, commandArgs, opts, dryRun);
}
2 changes: 2 additions & 0 deletions packages/core/src/corepack/index.ts
@@ -0,0 +1,2 @@
export * from './exec-package-manager';
export * from './is-corepack-enabled';
5 changes: 5 additions & 0 deletions packages/core/src/corepack/is-corepack-enabled.ts
@@ -0,0 +1,5 @@
export function isCorepackEnabled() {
// https://github.com/nodejs/corepack#environment-variables
// The COREPACK_ROOT environment variable is specifically set by Corepack to indicate that it is running.
return process.env['COREPACK_ROOT'] !== undefined;
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
@@ -1,4 +1,5 @@
// folders
export * from './corepack/index.js';
export * from './models/index.js';
export * from './package-graph/index.js';
export * from './project/index.js';
Expand Down
1 change: 1 addition & 0 deletions packages/core/tsconfig.json
Expand Up @@ -2,6 +2,7 @@
"extends": "../../tsconfig.base.json",
"compileOnSave": false,
"compilerOptions": {
"module": "ESNext",
"rootDir": "src",
"outDir": "dist",
"types": ["node"]
Expand Down
12 changes: 6 additions & 6 deletions packages/run/src/lib/__tests__/npm-run-script.spec.ts
Expand Up @@ -3,14 +3,14 @@ import { describe, expect, it, Mock, vi } from 'vitest';
// mocked modules
vi.mock('@lerna-lite/core');

import { exec, spawnStreaming } from '@lerna-lite/core';
import { execPackageManager, spawnStreaming } from '@lerna-lite/core';
import { RunScriptOption, ScriptStreamingOption } from '../../models';

// file under test
import { npmRunScript, npmRunScriptStreaming } from '../npm-run-script';

describe('npm-run-script', () => {
(exec as Mock).mockResolvedValue(null);
(execPackageManager as Mock).mockResolvedValue(null);
(spawnStreaming as Mock).mockResolvedValue(null);

describe('npmRunScript()', () => {
Expand All @@ -26,7 +26,7 @@ describe('npm-run-script', () => {

await npmRunScript(script, config);

expect(exec).toHaveBeenLastCalledWith(
expect(execPackageManager).toHaveBeenLastCalledWith(
'npm',
['run', script, '--bar', 'baz'],
{
Expand All @@ -52,7 +52,7 @@ describe('npm-run-script', () => {

await npmRunScript(script, config, true);

expect(exec).toHaveBeenLastCalledWith(
expect(execPackageManager).toHaveBeenLastCalledWith(
'npm',
['run', script, '--bar', 'baz'],
{
Expand All @@ -79,7 +79,7 @@ describe('npm-run-script', () => {

await npmRunScript(script, config);

expect(exec).toHaveBeenLastCalledWith(
expect(execPackageManager).toHaveBeenLastCalledWith(
'npm',
['run', script],
{
Expand All @@ -105,7 +105,7 @@ describe('npm-run-script', () => {

await npmRunScript(script, config);

expect(exec).toHaveBeenLastCalledWith(
expect(execPackageManager).toHaveBeenLastCalledWith(
'yarn',
['run', script, '--bar', 'baz'],
{
Expand Down
4 changes: 2 additions & 2 deletions packages/run/src/lib/npm-run-script.ts
@@ -1,5 +1,5 @@
import log from 'npmlog';
import { exec, Package, spawnStreaming } from '@lerna-lite/core';
import { execPackageManager, Package, spawnStreaming } from '@lerna-lite/core';

import { getNpmExecOpts } from './get-npm-exec-opts.js';
import { RunScriptOption, ScriptStreamingOption } from '../models/index.js';
Expand All @@ -10,7 +10,7 @@ export function npmRunScript(script: string, { args, npmClient, pkg, reject = tr
const argv = ['run', script, ...args];
const opts = makeOpts(pkg, reject);

return exec(npmClient, argv, opts, dryRun);
return execPackageManager(npmClient, argv, opts, dryRun);
}

export function npmRunScriptStreaming(
Expand Down

0 comments on commit 3f5624c

Please sign in to comment.