Files
react-native/scripts/__tests__/npm-utils-test.js
T
Luna Wei fe0306d637 Support specifying dist-tags for monorepo package bumps (#42146)
Summary:
Currently our CI will auto-tag any `npm publish` as `latest` for the monorepo packages. This is because [we do not specify a tag](https://github.com/facebook/react-native/blob/main/scripts/monorepo/find-and-publish-all-bumped-packages.js#L104), so npm will [default to `latest`](https://docs.npmjs.com/cli/v10/commands/npm-dist-tag#description). We encountered a similar issue for `react-native` awhile ago and fixed that with [always specifying a tag](https://github.com/facebook/react-native/blob/main/scripts/npm-utils.js#L84), with the explicit opt-in for `latest`.

yarn and npm will resolve `*` dependencies using `latest`. This will be a problem for any React Native version that uses `*` deps. We have actively tried to remove these `*` versions but older patches may still contain them.

When we do a monorepo package bump, it may be for 0.71 and for a user who is initializing a 0.72 version project (that still has * deps), they will receive monorepo packages of version `0.71.x`, which is not compatible. (React Native monorepo packages do not faithfully follow semver)

This change allows us to specify what tags to use and suggest tags based on what branch you are on and asks for confirmation

```
> branch 0.73-stable
? Select suggested npm tags. (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ "0.73-stable"
 ◉ "latest"
? Confirm these tags for *ALL* packages being bumped: "0.73-stable","latest" (Y/n)

> branch 0.72-stable
? Select suggested npm tags. (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ "0.72-stable"
 ◯ "latest"
? Confirm these tags for *ALL* packages being bumped: "0.72-stable" (Y/n)

> branch main
? Select suggested npm tags. (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ "nightly"
? Confirm these tags for *ALL* packages being bumped: "nightly" (Y/n)
```

## Changelog:

[INTERNAL] [CHANGED] - Support dist-tags in publishing monorepo packages to avoid default "latest" tag.

Pull Request resolved: https://github.com/facebook/react-native/pull/42146

Test Plan: `yarn test scripts/`

Reviewed By: NickGerleman

Differential Revision: D52551769

Pulled By: lunaleaps

fbshipit-source-id: 52f923464387cffdc6ca22c6f0a45425965a3680
2024-01-08 08:38:53 -08:00

186 lines
5.6 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
const {
applyPackageVersions,
getNpmInfo,
getPackageVersionStrByTag,
getVersionsBySpec,
publishPackage,
} = require('../npm-utils');
const execMock = jest.fn();
const getCurrentCommitMock = jest.fn();
jest.mock('shelljs', () => ({
exec: execMock,
}));
jest.mock('./../scm-utils', () => ({
getCurrentCommit: getCurrentCommitMock,
}));
describe('npm-utils', () => {
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();
});
describe('applyPackageVersions', () => {
it('should replace package.json with dependencies', () => {
const originalPackageJson = {
name: 'my-package',
dependencies: {
'my-dependency-a': 'nightly',
'my-dependency-b': '^1.2.3',
},
devDependencies: {
'my-dev-dependency-a': 'nightly',
'my-dev-dependency-b': '^1.2.3',
},
someOtherField: {
'my-dependency-a': 'should-be-untouched',
},
};
const dependencies = {
'my-dependency-a': '0.72.0-nightly-shortcommit',
'my-dev-dependency-a': 'updated-version',
'my-non-existant-dep': 'some-version',
};
const package = applyPackageVersions(originalPackageJson, dependencies);
expect(package).toEqual({
name: 'my-package',
dependencies: {
'my-dependency-a': '0.72.0-nightly-shortcommit',
'my-dependency-b': '^1.2.3',
},
devDependencies: {
'my-dev-dependency-a': 'updated-version',
'my-dev-dependency-b': '^1.2.3',
},
someOtherField: {
'my-dependency-a': 'should-be-untouched',
},
});
});
});
describe('getPackageVersionStrByTag', () => {
it('should return package version string', () => {
execMock.mockImplementationOnce(() => ({code: 0, stdout: '0.34.2 \n'}));
const versionStr = getPackageVersionStrByTag('my-package', 'next');
expect(versionStr).toBe('0.34.2');
});
it('should throw error when invalid result', () => {
execMock.mockImplementationOnce(() => ({
code: 1,
stderr: 'Some error message',
}));
expect(() => {
getPackageVersionStrByTag('my-package', 'next');
}).toThrow('Failed to get next version from npm\nSome error message');
});
});
describe('publishPackage', () => {
it('should run publish command', () => {
publishPackage(
'path/to/my-package',
{tags: ['latest'], otp: 'otp'},
{silent: true, cwd: 'i/expect/this/to/be/overriden'},
);
expect(execMock).toHaveBeenCalledWith(
'npm publish --tag latest --otp otp',
{silent: true, cwd: 'path/to/my-package'},
);
});
it('should run publish command when no execOptions', () => {
publishPackage('path/to/my-package', {tags: ['latest'], otp: 'otp'});
expect(execMock).toHaveBeenCalledWith(
'npm publish --tag latest --otp otp',
{cwd: 'path/to/my-package'},
);
});
it('should handle multiple tags', () => {
publishPackage('path/to/my-package', {
tags: ['next', '0.72-stable'],
otp: 'otp',
});
expect(execMock).toHaveBeenCalledWith(
'npm publish --tag next --tag 0.72-stable --otp otp',
{cwd: 'path/to/my-package'},
);
});
});
describe('getNpmInfo', () => {
it('return the expected format for prealpha', () => {
const isoStringSpy = jest.spyOn(Date.prototype, 'toISOString');
isoStringSpy.mockReturnValue('2023-10-04T15:43:55.123Z');
getCurrentCommitMock.mockImplementation(() => 'abcd1234');
const returnedValue = getNpmInfo('prealpha');
expect(returnedValue).toMatchObject({
version: `0.0.0-prealpha-2023100415`,
tag: 'prealpha',
});
});
});
describe('getVersionsBySpec', () => {
it('should return array when single version returned', () => {
execMock.mockImplementationOnce(() => ({code: 0, stdout: '"0.72.0" \n'}));
const versions = getVersionsBySpec('mypackage', '^0.72.0');
expect(versions).toEqual(['0.72.0']);
});
it('should return array of versions', () => {
execMock.mockImplementationOnce(() => ({
code: 0,
stdout: '[\n"0.73.0",\n"0.73.1"\n]\n',
}));
const versions = getVersionsBySpec('mypackage', '^0.73.0');
expect(versions).toEqual(['0.73.0', '0.73.1']);
});
it('should return error summary if E404', () => {
const error =
`npm ERR! code E404\n` +
`npm ERR! 404 No match found for version ^0.72.0\n` +
`npm ERR! 404\n` +
`npm ERR! 404 '@react-native/community-cli-plugin@^0.72.0' is not in this registry.\n` +
`npm ERR! 404\n` +
`npm ERR! 404 Note that you can also install from a\n` +
`npm ERR! 404 tarball, folder, http url, or git url.\n` +
`{\n` +
` "error": {\n` +
` "code": "E404",\n` +
` "summary": "No match found for version ^0.72.0",\n` +
` "detail": "\n '@react-native/community-cli-plugin@^0.72.0' is not in this registry.\n\nNote that you can also install from a\ntarball, folder, http url, or git url."\n` +
` }\n` +
`}\n`;
execMock.mockImplementationOnce(() => ({
code: 1,
stderr: error,
}));
expect(() => {
getVersionsBySpec('mypackage', '^0.72.0');
}).toThrow('No match found for version ^0.72.0');
});
});
});