Skip to content

Commit f969b0e

Browse files
authored
Update execa (#121)
This PR updates `execa` to `^8.0.1`. Since `execa@>=6.0.0` is ESM-only and `jest` only has experimental ESM support (jestjs/jest#10976), this required switching from `ts-jest` to `babel-jest`. To minimize dependency transpilation, the ESM packages that are necessary to transpile are enumerated in `jest.config.js`. This version of `execa` includes [automatic escaping of shell arguments](https://github.com/sindresorhus/execa/tree/v8.0.1#execafile-arguments-options), which was the entire point of #112, #113, and this PR. The state of ESM support in the Node.js ecosystem is absolutely horrible, and I would not recommend further migrations for the time being. We should continue to dual-release our packages and avoid ESM-only dependencies until the ecosystem has matured. For details see the above `jest` issue and nodejs/node#37648.
1 parent 858cb26 commit f969b0e

10 files changed

+2220
-482
lines changed

babel.config.cjs

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// We use Babel to transpile down ESM dependencies to CommonJS for our tests
2+
// using babel-jest.
3+
module.exports = {
4+
env: {
5+
test: {
6+
presets: ['@babel/preset-env', '@babel/preset-typescript'],
7+
plugins: ['@babel/plugin-transform-modules-commonjs']
8+
}
9+
}
10+
};

jest.config.cjs

+33-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,32 @@
33
* https://jestjs.io/docs/configuration
44
*/
55

6+
/**
7+
* Dependencies that are ESM-only, and need to be transpiled by Babel.
8+
* This list is used in the `transformIgnorePatterns` option below.
9+
*
10+
* You probably need to add a dependency to this list if the tests fail with something like:
11+
* - `SyntaxError: Cannot use import statement outside a module`
12+
* - `SyntaxError: Unexpected token 'export'`
13+
* If so, identify the dependency that's causing the error via the stack trace, and add it
14+
* to this list.
15+
*
16+
* No, we do not live in the best of all possible worlds. Why do you ask?
17+
*
18+
* For details on Jest's currently experimental ESM support see: https://github.com/jestjs/jest/issues/9430
19+
*/
20+
const ESM_DEPENDENCIES = [
21+
'execa',
22+
'strip-final-newline',
23+
'npm-run-path',
24+
'path-key',
25+
'onetime',
26+
'mimic-fn',
27+
'human-signals',
28+
'is-stream',
29+
'get-stream',
30+
];
31+
632
// This file needs to be .cjs for compatibility with jest-it-up.
733
module.exports = {
834
// All imported modules in your tests should be mocked automatically
@@ -103,8 +129,9 @@ module.exports = {
103129
// An enum that specifies notification mode. Requires { notify: true }
104130
// notifyMode: "failure-change",
105131

106-
// A preset that is used as a base for Jest's configuration
107-
preset: 'ts-jest',
132+
// Disabled in favor of babel-jest, configured via "transform" below.
133+
// // A preset that is used as a base for Jest's configuration
134+
// preset: 'ts-jest',
108135

109136
// Run tests from one or more projects
110137
// projects: undefined,
@@ -189,13 +216,12 @@ module.exports = {
189216
// timers: "real",
190217

191218
// A map from regular expressions to paths to transformers
192-
// transform: undefined,
219+
transform: {
220+
"\\.[jt]sx?$": "babel-jest"
221+
},
193222

194223
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
195-
// transformIgnorePatterns: [
196-
// "/node_modules/",
197-
// "\\.pnp\\.[^\\/]+$"
198-
// ],
224+
transformIgnorePatterns: [`node_modules/(?!(${ESM_DEPENDENCIES.join('|')}))`],
199225

200226
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
201227
// unmockedModulePathPatterns: undefined,

package.json

+8-4
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,33 @@
2929
"@metamask/auto-changelog": "~3.3.0",
3030
"@metamask/utils": "^8.2.1",
3131
"debug": "^4.3.4",
32-
"execa": "^5.1.1",
32+
"execa": "^8.0.1",
3333
"pony-cause": "^2.1.9",
3434
"semver": "^7.5.4",
3535
"which": "^3.0.0",
3636
"yaml": "^2.2.2",
3737
"yargs": "^17.7.1"
3838
},
3939
"devDependencies": {
40+
"@babel/core": "^7.23.5",
41+
"@babel/plugin-transform-modules-commonjs": "^7.23.3",
42+
"@babel/preset-env": "^7.23.5",
43+
"@babel/preset-typescript": "^7.23.3",
4044
"@lavamoat/allow-scripts": "^2.3.1",
4145
"@metamask/eslint-config": "^10.0.0",
4246
"@metamask/eslint-config-jest": "^10.0.0",
4347
"@metamask/eslint-config-nodejs": "^10.0.0",
4448
"@metamask/eslint-config-typescript": "^10.0.0",
4549
"@types/debug": "^4.1.7",
46-
"@types/jest": "^29.5.1",
50+
"@types/jest": "^29.5.10",
4751
"@types/jest-when": "^3.5.2",
4852
"@types/node": "^17.0.23",
4953
"@types/rimraf": "^4.0.5",
5054
"@types/which": "^3.0.0",
5155
"@types/yargs": "^17.0.10",
5256
"@typescript-eslint/eslint-plugin": "^5.62.0",
5357
"@typescript-eslint/parser": "^5.62.0",
58+
"babel-jest": "^29.7.0",
5459
"deepmerge": "^4.2.2",
5560
"eslint": "^8.27.0",
5661
"eslint-config-prettier": "^8.5.0",
@@ -59,15 +64,14 @@
5964
"eslint-plugin-jsdoc": "^39.6.2",
6065
"eslint-plugin-node": "^11.1.0",
6166
"eslint-plugin-prettier": "^4.2.1",
62-
"jest": "^29.5.0",
67+
"jest": "^29.7.0",
6368
"jest-it-up": "^3.0.0",
6469
"jest-when": "^3.5.2",
6570
"nanoid": "^3.3.4",
6671
"prettier": "^2.2.1",
6772
"prettier-plugin-packagejson": "^2.3.0",
6873
"rimraf": "^4.0.5",
6974
"stdio-mock": "^1.2.0",
70-
"ts-jest": "^29.1.0",
7175
"tsx": "^4.6.1",
7276
"typescript": "~5.1.6"
7377
},

src/misc-utils.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ describe('misc-utils', () => {
137137
describe('runCommand', () => {
138138
it('runs the command, discarding its output', async () => {
139139
const execaSpy = jest
140-
.spyOn(execaModule, 'default')
140+
.spyOn(execaModule, 'execa')
141141
// Typecast: It's difficult to provide a full return value for execa
142142
.mockResolvedValue({ stdout: ' some output ' } as any);
143143

@@ -155,7 +155,7 @@ describe('misc-utils', () => {
155155
describe('getStdoutFromCommand', () => {
156156
it('executes the given command and returns a version of the standard out from the command with whitespace trimmed', async () => {
157157
const execaSpy = jest
158-
.spyOn(execaModule, 'default')
158+
.spyOn(execaModule, 'execa')
159159
// Typecast: It's difficult to provide a full return value for execa
160160
.mockResolvedValue({ stdout: ' some output ' } as any);
161161

@@ -175,7 +175,7 @@ describe('misc-utils', () => {
175175
describe('getLinesFromCommand', () => {
176176
it('executes the given command and returns the standard out from the command split into lines', async () => {
177177
const execaSpy = jest
178-
.spyOn(execaModule, 'default')
178+
.spyOn(execaModule, 'execa')
179179
// Typecast: It's difficult to provide a full return value for execa
180180
.mockResolvedValue({ stdout: 'line 1\nline 2\nline 3' } as any);
181181

@@ -193,7 +193,7 @@ describe('misc-utils', () => {
193193

194194
it('does not strip leading and trailing whitespace from the output, but does remove empty lines', async () => {
195195
const execaSpy = jest
196-
.spyOn(execaModule, 'default')
196+
.spyOn(execaModule, 'execa')
197197
// Typecast: It's difficult to provide a full return value for execa
198198
.mockResolvedValue({
199199
stdout: ' line 1\nline 2\n\n line 3 \n',

src/misc-utils.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import which from 'which';
2-
import execa from 'execa';
2+
import { execa, Options } from 'execa';
33
import createDebug from 'debug';
44
import { ErrorWithCause } from 'pony-cause';
55
import { isObject } from '@metamask/utils';
@@ -130,7 +130,7 @@ export async function resolveExecutable(
130130
export async function runCommand(
131131
command: string,
132132
args?: readonly string[] | undefined,
133-
options?: execa.Options<string> | undefined,
133+
options?: Options | undefined,
134134
): Promise<void> {
135135
await execa(command, args, options);
136136
}
@@ -149,7 +149,7 @@ export async function runCommand(
149149
export async function getStdoutFromCommand(
150150
command: string,
151151
args?: readonly string[] | undefined,
152-
options?: execa.Options<string> | undefined,
152+
options?: Options | undefined,
153153
): Promise<string> {
154154
return (await execa(command, args, options)).stdout.trim();
155155
}
@@ -167,7 +167,7 @@ export async function getStdoutFromCommand(
167167
export async function getLinesFromCommand(
168168
command: string,
169169
args?: readonly string[] | undefined,
170-
options?: execa.Options<string> | undefined,
170+
options?: Options | undefined,
171171
): Promise<string[]> {
172172
const { stdout } = await execa(command, args, options);
173173
return stdout.split('\n').filter((value) => value !== '');

src/monorepo-workflow-operations.test.ts

+82
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,88 @@ describe('monorepo-workflow-operations', () => {
395395
});
396396
});
397397

398+
it('follows the workflow correctly when executed twice', async () => {
399+
await withSandbox(async (sandbox) => {
400+
const releaseVersion = '1.1.0';
401+
const {
402+
project,
403+
stdout,
404+
stderr,
405+
createReleaseBranchSpy,
406+
commitAllChangesSpy,
407+
projectDirectoryPath,
408+
} = await setupFollowMonorepoWorkflow({
409+
sandbox,
410+
releaseVersion,
411+
doesReleaseSpecFileExist: false,
412+
isEditorAvailable: true,
413+
});
414+
415+
createReleaseBranchSpy.mockResolvedValueOnce({
416+
version: releaseVersion,
417+
firstRun: true,
418+
});
419+
420+
await followMonorepoWorkflow({
421+
project,
422+
tempDirectoryPath: sandbox.directoryPath,
423+
firstRemovingExistingReleaseSpecification: false,
424+
releaseType: 'ordinary',
425+
defaultBranch: 'main',
426+
stdout,
427+
stderr,
428+
});
429+
430+
expect(createReleaseBranchSpy).toHaveBeenCalledTimes(1);
431+
expect(createReleaseBranchSpy).toHaveBeenLastCalledWith({
432+
project,
433+
releaseType: 'ordinary',
434+
});
435+
436+
expect(commitAllChangesSpy).toHaveBeenCalledTimes(2);
437+
expect(commitAllChangesSpy).toHaveBeenNthCalledWith(
438+
1,
439+
projectDirectoryPath,
440+
`Initialize Release ${releaseVersion}`,
441+
);
442+
expect(commitAllChangesSpy).toHaveBeenNthCalledWith(
443+
2,
444+
projectDirectoryPath,
445+
`Update Release ${releaseVersion}`,
446+
);
447+
448+
// Second call of followMonorepoWorkflow
449+
450+
createReleaseBranchSpy.mockResolvedValueOnce({
451+
version: releaseVersion,
452+
firstRun: false, // It's no longer the first run
453+
});
454+
455+
await followMonorepoWorkflow({
456+
project,
457+
tempDirectoryPath: sandbox.directoryPath,
458+
firstRemovingExistingReleaseSpecification: false,
459+
releaseType: 'ordinary',
460+
defaultBranch: 'main',
461+
stdout,
462+
stderr,
463+
});
464+
465+
expect(createReleaseBranchSpy).toHaveBeenCalledTimes(2);
466+
expect(createReleaseBranchSpy).toHaveBeenLastCalledWith({
467+
project,
468+
releaseType: 'ordinary',
469+
});
470+
471+
expect(commitAllChangesSpy).toHaveBeenCalledTimes(3);
472+
expect(commitAllChangesSpy).toHaveBeenNthCalledWith(
473+
3,
474+
projectDirectoryPath,
475+
`Update Release ${releaseVersion}`,
476+
);
477+
});
478+
});
479+
398480
it('attempts to execute the release spec if it was successfully edited', async () => {
399481
await withSandbox(async (sandbox) => {
400482
const {

src/repo.ts

+1
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ export async function hasChangesInDirectorySinceGitTag(
309309
tagName,
310310
);
311311

312+
/* istanbul ignore else */
312313
if (!(tagName in CHANGED_FILE_PATHS_BY_TAG_NAME)) {
313314
CHANGED_FILE_PATHS_BY_TAG_NAME[tagName] = changedFilePaths;
314315
}

tests/functional/helpers/monorepo-environment.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from 'fs';
22
import path from 'path';
3-
import { ExecaReturnValue } from 'execa';
3+
import type { ExecaReturnValue } from 'execa';
44
import YAML from 'yaml';
55
import { TOOL_EXECUTABLE_PATH, TSX_PATH } from './constants.js';
66
import Environment, {

tests/functional/helpers/repo.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from 'fs';
22
import path from 'path';
3-
import execa, { ExecaChildProcess, Options as ExecaOptions } from 'execa';
3+
import { execa, ExecaChildProcess, Options as ExecaOptions } from 'execa';
44
import deepmerge from 'deepmerge';
55
import { isErrorWithCode } from '../../helpers.js';
66
import { debug, sleepFor } from './utils.js';
@@ -178,7 +178,7 @@ export default abstract class Repo {
178178
async runCommand(
179179
executableName: string,
180180
args?: readonly string[] | undefined,
181-
options?: ExecaOptions<string> | undefined,
181+
options?: ExecaOptions | undefined,
182182
): Promise<ExecaChildProcess<string>> {
183183
const { env, ...remainingOptions } =
184184
options === undefined ? { env: {} } : options;

0 commit comments

Comments
 (0)