Refactor set-rn-version and add tests (#37291)

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

Changelog: [Internal] Add tests for `rn-set-version` script and refactor for clarity

Reviewed By: NickGerleman

Differential Revision: D45248915

fbshipit-source-id: f6b8566d553d0954647ba81c55767fab6a2caf1c
This commit is contained in:
Luna Wei 2023-05-16 16:44:34 -07:00 коммит произвёл Facebook GitHub Bot
Родитель 2c4e809beb
Коммит b23cf101f7
2 изменённых файлов: 326 добавлений и 146 удалений

Просмотреть файл

@ -0,0 +1,182 @@
/**
* 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 execMock = jest.fn();
const echoMock = jest.fn();
const exitMock = jest.fn();
const catMock = jest.fn();
const sedMock = jest.fn();
const writeFileSyncMock = jest.fn();
jest
.mock('shelljs', () => ({
exec: execMock,
echo: echoMock,
exit: exitMock,
cat: catMock,
sed: sedMock,
}))
.mock('./../scm-utils', () => ({
saveFiles: jest.fn(),
}))
.mock('path', () => ({
join: () => '../packages/react-native',
}))
.mock('fs', () => ({
writeFileSync: writeFileSyncMock,
mkdtempSync: () => './rn-set-version/',
}))
.mock('os');
const setReactNativeVersion = require('../set-rn-version');
describe('set-rn-version', () => {
afterEach(() => {
jest.resetModules();
jest.resetAllMocks();
});
it('should set nightly version', () => {
catMock.mockImplementation(path => {
if (path === 'packages/react-native/package.json') {
return '{"name": "myPackage", "version": 2}';
} else if (
path === 'scripts/versiontemplates/ReactNativeVersion.java.template' ||
path === 'scripts/versiontemplates/RCTVersion.m.template' ||
path === 'scripts/versiontemplates/ReactNativeVersion.h.template' ||
path === 'scripts/versiontemplates/ReactNativeVersion.js.template'
) {
return '{major: ${major}, minor: ${minor}, patch: ${patch}, prerelease: ${prerelease}}';
} else {
throw new Error(`Invalid path passed for package dir. Path: ${path}`);
}
});
execMock
.mockReturnValueOnce({code: 0})
.mockReturnValueOnce({stdout: 'line1\nline2\nline3\n'});
sedMock.mockReturnValueOnce({code: 0});
const version = '0.81.0-nightly-29282302-abcd1234';
setReactNativeVersion(version, 'nightly');
expect(sedMock).toHaveBeenCalledWith(
'-i',
/^VERSION_NAME=.*/,
`VERSION_NAME=${version}`,
'packages/react-native/ReactAndroid/gradle.properties',
);
expect(writeFileSyncMock.mock.calls.length).toBe(5);
expect(writeFileSyncMock.mock.calls[0][0]).toBe(
'packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java',
);
expect(writeFileSyncMock.mock.calls[0][1]).toBe(
'{major: 0, minor: 81, patch: 0, prerelease: "nightly-29282302-abcd1234"}',
);
expect(writeFileSyncMock.mock.calls[1][0]).toBe(
'packages/react-native/React/Base/RCTVersion.m',
);
expect(writeFileSyncMock.mock.calls[2][0]).toBe(
'packages/react-native/ReactCommon/cxxreact/ReactNativeVersion.h',
);
expect(writeFileSyncMock.mock.calls[3][0]).toBe(
'packages/react-native/Libraries/Core/ReactNativeVersion.js',
);
expect(writeFileSyncMock.mock.calls[4][0]).toBe(
'packages/react-native/package.json',
);
expect(writeFileSyncMock.mock.calls[4][1]).toBe(
`{\n "name": "myPackage",\n "version": "${version}"\n}`,
);
expect(exitMock.mock.calls[0][0]).toBe(0);
expect(execMock.mock.calls[0][0]).toBe(
`node scripts/set-rn-template-version.js ${version}`,
);
});
it('should set release version', () => {
catMock.mockImplementation(path => {
if (path === 'packages/react-native/package.json') {
return '{"name": "myPackage", "version": 2}';
}
return 'exports.version = {major: ${major}, minor: ${minor}, patch: ${patch}, prerelease: ${prerelease}}';
});
execMock
.mockReturnValueOnce({code: 0})
.mockReturnValueOnce({stdout: 'line1\nline2\nline3\n'});
sedMock.mockReturnValueOnce({code: 0});
const version = '0.81.0';
setReactNativeVersion(version, 'release');
expect(sedMock).toHaveBeenCalledWith(
'-i',
/^VERSION_NAME=.*/,
`VERSION_NAME=${version}`,
'packages/react-native/ReactAndroid/gradle.properties',
);
expect(writeFileSyncMock.mock.calls.length).toBe(5);
expect(writeFileSyncMock.mock.calls[0][0]).toBe(
'packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java',
);
expect(writeFileSyncMock.mock.calls[0][1]).toBe(
'exports.version = {major: 0, minor: 81, patch: 0, prerelease: null}',
);
expect(writeFileSyncMock.mock.calls[4][0]).toBe(
'packages/react-native/package.json',
);
expect(writeFileSyncMock.mock.calls[4][1]).toBe(
`{\n "name": "myPackage",\n "version": "${version}"\n}`,
);
expect(exitMock.mock.calls[0][0]).toBe(0);
expect(execMock.mock.calls[0][0]).toBe(
`node scripts/set-rn-template-version.js ${version}`,
);
expect(execMock.mock.calls[1][0]).toBe(
`diff -r ./rn-set-version/ . | grep '^[>]' | grep -c ${version} `,
);
});
it('should fail validation', () => {
catMock.mockReturnValue('{}');
execMock
.mockReturnValueOnce({code: 0})
.mockReturnValueOnce({stdout: 'line1\nline2\n'});
sedMock.mockReturnValueOnce({code: 0});
const filesToValidate = [
'packages/react-native/package.json',
'packages/react-native/ReactAndroid/gradle.properties',
'packages/react-native/template/package.json',
];
const version = '0.81.0';
setReactNativeVersion(version, 'release');
expect(exitMock).toHaveBeenCalledWith(0);
expect(echoMock).toHaveBeenNthCalledWith(
1,
'The tmp versioning folder is ./rn-set-version/',
);
expect(echoMock).toHaveBeenNthCalledWith(2, 'WARNING:');
expect(echoMock.mock.calls[2][0]).toBe(
`Failed to update all the files: [${filesToValidate.join(
', ',
)}] must have versions in them`,
);
expect(echoMock.mock.calls[3][0]).toBe(
`These files already had version ${version} set.`,
);
});
});

Просмотреть файл

@ -9,13 +9,6 @@
'use strict';
/**
* This script updates relevant React Native files with supplied version:
* * Prepares a package.json suitable for package consumption
* * Updates package.json for template project
* * Updates the version in gradle files and makes sure they are consistent between each other
* * Creates a gemfile
*/
const fs = require('fs');
const os = require('os');
const path = require('path');
@ -24,155 +17,160 @@ const yargs = require('yargs');
const {parseVersion, validateBuildType} = require('./version-utils');
const {saveFiles} = require('./scm-utils');
let argv = yargs
.option('v', {
alias: 'to-version',
type: 'string',
required: true,
})
.option('b', {
alias: 'build-type',
type: 'string',
required: true,
}).argv;
/**
* This script updates relevant React Native files with supplied version:
* * Prepares a package.json suitable for package consumption
* * Updates package.json for template project
* * Updates the version in gradle files and makes sure they are consistent between each other
* * Creates a gemfile
*/
if (require.main === module) {
let argv = yargs
.option('v', {
alias: 'to-version',
type: 'string',
required: true,
})
.option('b', {
alias: 'build-type',
type: 'string',
required: true,
}).argv;
const buildType = argv.buildType;
const version = argv.toVersion;
try {
validateBuildType(buildType);
} catch (e) {
throw e;
setReactNativeVersion(argv.toVersion, argv.buildType);
}
let major,
minor,
patch,
prerelease = -1;
try {
({major, minor, patch, prerelease} = parseVersion(version, buildType));
} catch (e) {
throw e;
function setSource({major, minor, patch, prerelease}) {
fs.writeFileSync(
'packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java',
cat('scripts/versiontemplates/ReactNativeVersion.java.template')
.replace('${major}', major)
.replace('${minor}', minor)
.replace('${patch}', patch)
.replace(
'${prerelease}',
prerelease !== undefined ? `"${prerelease}"` : 'null',
),
'utf-8',
);
fs.writeFileSync(
'packages/react-native/React/Base/RCTVersion.m',
cat('scripts/versiontemplates/RCTVersion.m.template')
.replace('${major}', `@(${major})`)
.replace('${minor}', `@(${minor})`)
.replace('${patch}', `@(${patch})`)
.replace(
'${prerelease}',
prerelease !== undefined ? `@"${prerelease}"` : '[NSNull null]',
),
'utf-8',
);
fs.writeFileSync(
'packages/react-native/ReactCommon/cxxreact/ReactNativeVersion.h',
cat('scripts/versiontemplates/ReactNativeVersion.h.template')
.replace('${major}', major)
.replace('${minor}', minor)
.replace('${patch}', patch)
.replace(
'${prerelease}',
prerelease !== undefined ? `"${prerelease}"` : '""',
),
'utf-8',
);
fs.writeFileSync(
'packages/react-native/Libraries/Core/ReactNativeVersion.js',
cat('scripts/versiontemplates/ReactNativeVersion.js.template')
.replace('${major}', major)
.replace('${minor}', minor)
.replace('${patch}', patch)
.replace(
'${prerelease}',
prerelease !== undefined ? `'${prerelease}'` : 'null',
),
'utf-8',
);
}
const tmpVersioningFolder = fs.mkdtempSync(
path.join(os.tmpdir(), 'rn-set-version'),
);
echo(`The temp versioning folder is ${tmpVersioningFolder}`);
saveFiles(
[
'packages/react-native/package.json',
'packages/react-native/template/package.json',
],
tmpVersioningFolder,
);
fs.writeFileSync(
'packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java',
cat('scripts/versiontemplates/ReactNativeVersion.java.template')
.replace('${major}', major)
.replace('${minor}', minor)
.replace('${patch}', patch)
.replace(
'${prerelease}',
prerelease !== undefined ? `"${prerelease}"` : 'null',
),
'utf-8',
);
fs.writeFileSync(
'packages/react-native/React/Base/RCTVersion.m',
cat('scripts/versiontemplates/RCTVersion.m.template')
.replace('${major}', `@(${major})`)
.replace('${minor}', `@(${minor})`)
.replace('${patch}', `@(${patch})`)
.replace(
'${prerelease}',
prerelease !== undefined ? `@"${prerelease}"` : '[NSNull null]',
),
'utf-8',
);
fs.writeFileSync(
'packages/react-native/ReactCommon/cxxreact/ReactNativeVersion.h',
cat('scripts/versiontemplates/ReactNativeVersion.h.template')
.replace('${major}', major)
.replace('${minor}', minor)
.replace('${patch}', patch)
.replace(
'${prerelease}',
prerelease !== undefined ? `"${prerelease}"` : '""',
),
'utf-8',
);
fs.writeFileSync(
'packages/react-native/Libraries/Core/ReactNativeVersion.js',
cat('scripts/versiontemplates/ReactNativeVersion.js.template')
.replace('${major}', major)
.replace('${minor}', minor)
.replace('${patch}', patch)
.replace(
'${prerelease}',
prerelease !== undefined ? `'${prerelease}'` : 'null',
),
'utf-8',
);
const packageJson = JSON.parse(cat('packages/react-native/package.json'));
packageJson.version = version;
fs.writeFileSync(
'packages/react-native/package.json',
JSON.stringify(packageJson, null, 2),
'utf-8',
);
// Change ReactAndroid/gradle.properties
saveFiles(
['packages/react-native/ReactAndroid/gradle.properties'],
tmpVersioningFolder,
);
if (
sed(
function setGradle({version}) {
const result = sed(
'-i',
/^VERSION_NAME=.*/,
`VERSION_NAME=${version}`,
'packages/react-native/ReactAndroid/gradle.properties',
).code
) {
echo("Couldn't update version for Gradle");
exit(1);
}
// Change react-native version in the template's package.json
exec(`node scripts/set-rn-template-version.js ${version}`);
// Verify that files changed, we just do a git diff and check how many times version is added across files
const filesToValidate = [
'packages/react-native/package.json',
'packages/react-native/ReactAndroid/gradle.properties',
'packages/react-native/template/package.json',
];
const numberOfChangedLinesWithNewVersion = exec(
`diff -r ${tmpVersioningFolder} . | grep '^[>]' | grep -c ${version} `,
{silent: true},
).stdout.trim();
if (+numberOfChangedLinesWithNewVersion !== filesToValidate.length) {
// TODO: the logic that checks whether all the changes have been applied
// is missing several files. For example, it is not checking Ruby version nor that
// the Objecive-C files, the codegen and other files are properly updated.
// We are going to work on this in another PR.
echo('WARNING:');
echo(
`Failed to update all the files: [${filesToValidate.join(
', ',
)}] must have versions in them`,
);
echo(`These files already had version ${version} set.`);
if (result.code) {
echo("Couldn't update version for Gradle");
throw result.stderr;
}
}
exit(0);
function setPackage({version}) {
const packageJson = JSON.parse(cat('packages/react-native/package.json'));
packageJson.version = version;
fs.writeFileSync(
'packages/react-native/package.json',
JSON.stringify(packageJson, null, 2),
'utf-8',
);
}
function setTemplatePackage({version}) {
const result = exec(`node scripts/set-rn-template-version.js ${version}`);
if (result.code) {
echo("Failed to update React Native template's version of React Native");
throw result.stderr;
}
}
function setReactNativeVersion(argVersion, buildType) {
validateBuildType(buildType);
const version = parseVersion(argVersion, buildType);
// Create tmp folder for copies of files to verify files have changed
const filesToValidate = [
'packages/react-native/package.json',
'packages/react-native/ReactAndroid/gradle.properties',
'packages/react-native/template/package.json',
];
const tmpVersioningFolder = fs.mkdtempSync(
path.join(os.tmpdir(), 'rn-set-version'),
);
echo(`The tmp versioning folder is ${tmpVersioningFolder}`);
saveFiles(tmpVersioningFolder);
setSource(version);
setPackage(version);
setTemplatePackage(version);
setGradle(version);
// Validate changes
// We just do a git diff and check how many times version is added across files
const numberOfChangedLinesWithNewVersion = exec(
`diff -r ${tmpVersioningFolder} . | grep '^[>]' | grep -c ${version.version} `,
{silent: true},
).stdout.trim();
if (+numberOfChangedLinesWithNewVersion !== filesToValidate.length) {
// TODO: the logic that checks whether all the changes have been applied
// is missing several files. For example, it is not checking Ruby version nor that
// the Objecive-C files, the codegen and other files are properly updated.
// We are going to work on this in another PR.
echo('WARNING:');
echo(
`Failed to update all the files: [${filesToValidate.join(
', ',
)}] must have versions in them`,
);
echo(`These files already had version ${version.version} set.`);
}
return exit(0);
}
module.exports = setReactNativeVersion;