From b23cf101f7c0adeb1dec908a199cac8a05ef3a3d Mon Sep 17 00:00:00 2001 From: Luna Wei Date: Tue, 16 May 2023 16:44:34 -0700 Subject: [PATCH] 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 --- scripts/__tests__/set-rn-version-test.js | 182 ++++++++++++++ scripts/set-rn-version.js | 290 +++++++++++------------ 2 files changed, 326 insertions(+), 146 deletions(-) create mode 100644 scripts/__tests__/set-rn-version-test.js diff --git a/scripts/__tests__/set-rn-version-test.js b/scripts/__tests__/set-rn-version-test.js new file mode 100644 index 00000000000..b913e4638b9 --- /dev/null +++ b/scripts/__tests__/set-rn-version-test.js @@ -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.`, + ); + }); +}); diff --git a/scripts/set-rn-version.js b/scripts/set-rn-version.js index 3d525b93e25..a5f99213740 100755 --- a/scripts/set-rn-version.js +++ b/scripts/set-rn-version.js @@ -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;