diff --git a/.circleci/config.yml b/.circleci/config.yml index 8a7d38a0b3f1df..96523f6d3a1f91 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -795,7 +795,7 @@ jobs: name: Create Android template project command: | REPO_ROOT=$(pwd) - node ./scripts/set-rn-template-version.js "file:$REPO_ROOT/build/$(cat build/react-native-package-version)" + node ./scripts/update-template-package.js "{\"react-native\":\"file:$REPO_ROOT/build/$(cat build/react-native-package-version)\"}" node ./scripts/template/initialize.js --reactNativeRootPath $REPO_ROOT --templateName $PROJECT_NAME --templateConfigPath "$REPO_ROOT/packages/react-native" --directory "/tmp/$PROJECT_NAME" - run: name: Build the template application for << parameters.flavor >> with Architecture set to << parameters.architecture >>, and using the << parameters.jsengine>> JS engine. @@ -879,7 +879,7 @@ jobs: REPO_ROOT=$(pwd) PACKAGE=$(cat build/react-native-package-version) PATH_TO_PACKAGE="$REPO_ROOT/build/$PACKAGE" - node ./scripts/set-rn-template-version.js "file:$PATH_TO_PACKAGE" + node ./scripts/update-template-package.js "{\"react-native\":\"file:$PATH_TO_PACKAGE\"}" node ./scripts/template/initialize.js --reactNativeRootPath $REPO_ROOT --templateName $PROJECT_NAME --templateConfigPath "$REPO_ROOT/packages/react-native" --directory "/tmp/$PROJECT_NAME" - run: name: Install iOS dependencies - Configuration << parameters.flavor >>; New Architecture << parameters.architecture >>; JS Engine << parameters.jsengine>>; Flipper << parameters.flipper >> diff --git a/scripts/__tests__/npm-utils-test.js b/scripts/__tests__/npm-utils-test.js index bcf10031df3a85..b8013fcebd3e74 100644 --- a/scripts/__tests__/npm-utils-test.js +++ b/scripts/__tests__/npm-utils-test.js @@ -7,7 +7,11 @@ * @format */ -const {getPackageVersionStrByTag, publishPackage} = require('../npm-utils'); +const { + applyPackageVersions, + getPackageVersionStrByTag, + publishPackage, +} = require('../npm-utils'); const execMock = jest.fn(); jest.mock('shelljs', () => ({ @@ -20,6 +24,47 @@ describe('npm-utils', () => { 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'})); diff --git a/scripts/__tests__/publish-npm-test.js b/scripts/__tests__/publish-npm-test.js index 3f2b88626a8728..3af1fa8bb3473e 100644 --- a/scripts/__tests__/publish-npm-test.js +++ b/scripts/__tests__/publish-npm-test.js @@ -10,7 +10,9 @@ const execMock = jest.fn(); const echoMock = jest.fn(); const exitMock = jest.fn(); +const consoleErrorMock = jest.fn(); const isTaggedLatestMock = jest.fn(); +const setReactNativeVersionMock = jest.fn(); const publishAndroidArtifactsToMavenMock = jest.fn(); const env = process.env; @@ -33,11 +35,13 @@ jest generateAndroidArtifacts: jest.fn(), publishAndroidArtifactsToMaven: publishAndroidArtifactsToMavenMock, })) + .mock('./../set-rn-version', () => setReactNativeVersionMock) .mock('../monorepo/get-and-update-nightlies'); const date = new Date('2023-04-20T23:52:39.543Z'); const publishNpm = require('../publish-npm'); +let consoleError; describe('publish-npm', () => { beforeAll(() => { @@ -47,8 +51,14 @@ describe('publish-npm', () => { afterAll(() => { jest.useRealTimers(); }); + beforeEach(() => { + consoleError = console.error; + console.error = consoleErrorMock; + }); + afterEach(() => { process.env = env; + console.error = consoleError; }); afterEach(() => { @@ -58,8 +68,6 @@ describe('publish-npm', () => { describe('dry-run', () => { it('should set version and not publish', () => { - execMock.mockReturnValueOnce({code: 0}); - publishNpm('dry-run'); expect(exitMock).toHaveBeenCalledWith(0); @@ -67,10 +75,11 @@ describe('publish-npm', () => { expect(echoMock).toHaveBeenCalledWith( 'Skipping `npm publish` because --dry-run is set.', ); - expect(execMock).toHaveBeenCalledWith( - 'node scripts/set-rn-version.js --to-version 1000.0.0-currentco --build-type dry-run', + expect(setReactNativeVersionMock).toBeCalledWith( + '1000.0.0-currentco', + null, + 'dry-run', ); - expect(execMock.mock.calls).toHaveLength(1); }); }); @@ -78,7 +87,6 @@ describe('publish-npm', () => { it('should publish', () => { execMock .mockReturnValueOnce({stdout: '0.81.0-rc.1\n', code: 0}) - .mockReturnValueOnce({code: 0}) .mockReturnValueOnce({code: 0}); const expectedVersion = '0.82.0-nightly-20230420-currentco'; @@ -91,22 +99,19 @@ describe('publish-npm', () => { expect(execMock.mock.calls[0][0]).toBe( `npm view react-native@next version`, ); - expect(execMock.mock.calls[1][0]).toBe( - `node scripts/set-rn-version.js --to-version ${expectedVersion} --build-type nightly`, - ); - expect(execMock.mock.calls[2][0]).toBe('npm publish --tag nightly'); + expect(execMock.mock.calls[1][0]).toBe('npm publish --tag nightly'); expect(echoMock).toHaveBeenCalledWith( `Published to npm ${expectedVersion}`, ); expect(exitMock).toHaveBeenCalledWith(0); - expect(execMock.mock.calls).toHaveLength(3); }); it('should fail to set version', () => { - execMock - .mockReturnValueOnce({stdout: '0.81.0-rc.1\n', code: 0}) - .mockReturnValueOnce({code: 1}); + execMock.mockReturnValueOnce({stdout: '0.81.0-rc.1\n', code: 0}); const expectedVersion = '0.82.0-nightly-20230420-currentco'; + setReactNativeVersionMock.mockImplementation(() => { + throw new Error('something went wrong'); + }); publishNpm('nightly'); @@ -114,14 +119,10 @@ describe('publish-npm', () => { expect(execMock.mock.calls[0][0]).toBe( `npm view react-native@next version`, ); - expect(execMock.mock.calls[1][0]).toBe( - `node scripts/set-rn-version.js --to-version ${expectedVersion} --build-type nightly`, - ); - expect(echoMock).toHaveBeenCalledWith( + expect(consoleErrorMock).toHaveBeenCalledWith( `Failed to set version number to ${expectedVersion}`, ); expect(exitMock).toHaveBeenCalledWith(1); - expect(execMock.mock.calls).toHaveLength(2); }); }); diff --git a/scripts/__tests__/set-rn-version-test.js b/scripts/__tests__/set-rn-version-test.js index b913e4638b9a3f..5c83cb002ee945 100644 --- a/scripts/__tests__/set-rn-version-test.js +++ b/scripts/__tests__/set-rn-version-test.js @@ -9,19 +9,19 @@ const execMock = jest.fn(); const echoMock = jest.fn(); -const exitMock = jest.fn(); const catMock = jest.fn(); const sedMock = jest.fn(); const writeFileSyncMock = jest.fn(); +const updateTemplatePackageMock = jest.fn(); jest .mock('shelljs', () => ({ exec: execMock, echo: echoMock, - exit: exitMock, cat: catMock, sed: sedMock, })) + .mock('./../update-template-package', () => updateTemplatePackageMock) .mock('./../scm-utils', () => ({ saveFiles: jest.fn(), })) @@ -45,7 +45,7 @@ describe('set-rn-version', () => { it('should set nightly version', () => { catMock.mockImplementation(path => { if (path === 'packages/react-native/package.json') { - return '{"name": "myPackage", "version": 2}'; + return '{"name": "myPackage", "version": 2, "dependencies": {"@react-native/package-a": "nightly", "@react-native/package-b": "^0.73.0"}}'; } else if ( path === 'scripts/versiontemplates/ReactNativeVersion.java.template' || path === 'scripts/versiontemplates/RCTVersion.m.template' || @@ -58,13 +58,14 @@ describe('set-rn-version', () => { } }); - execMock - .mockReturnValueOnce({code: 0}) - .mockReturnValueOnce({stdout: 'line1\nline2\nline3\n'}); + execMock.mockReturnValueOnce({stdout: 'line1\nline2\nline3\n'}); sedMock.mockReturnValueOnce({code: 0}); const version = '0.81.0-nightly-29282302-abcd1234'; - setReactNativeVersion(version, 'nightly'); + const nightlyVersions = { + '@react-native/package-a': version, + }; + setReactNativeVersion(version, nightlyVersions, 'nightly'); expect(sedMock).toHaveBeenCalledWith( '-i', @@ -91,14 +92,19 @@ describe('set-rn-version', () => { 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(writeFileSyncMock.mock.calls[4][1]).toBe(`{ + "name": "myPackage", + "version": "${version}", + "dependencies": { + "@react-native/package-a": "0.81.0-nightly-29282302-abcd1234", + "@react-native/package-b": "^0.73.0" + } +}`); + + expect(updateTemplatePackageMock).toHaveBeenCalledWith({ + '@react-native/package-a': '0.81.0-nightly-29282302-abcd1234', + 'react-native': version, + }); }); it('should set release version', () => { @@ -109,13 +115,11 @@ describe('set-rn-version', () => { return 'exports.version = {major: ${major}, minor: ${minor}, patch: ${patch}, prerelease: ${prerelease}}'; }); - execMock - .mockReturnValueOnce({code: 0}) - .mockReturnValueOnce({stdout: 'line1\nline2\nline3\n'}); + execMock.mockReturnValueOnce({stdout: 'line1\nline2\nline3\n'}); sedMock.mockReturnValueOnce({code: 0}); const version = '0.81.0'; - setReactNativeVersion(version, 'release'); + setReactNativeVersion(version, null, 'release'); expect(sedMock).toHaveBeenCalledWith( '-i', @@ -137,11 +141,10 @@ describe('set-rn-version', () => { `{\n "name": "myPackage",\n "version": "${version}"\n}`, ); - expect(exitMock.mock.calls[0][0]).toBe(0); + expect(updateTemplatePackageMock).toHaveBeenCalledWith({ + 'react-native': version, + }); 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} `, ); }); @@ -149,9 +152,7 @@ describe('set-rn-version', () => { it('should fail validation', () => { catMock.mockReturnValue('{}'); - execMock - .mockReturnValueOnce({code: 0}) - .mockReturnValueOnce({stdout: 'line1\nline2\n'}); + execMock.mockReturnValueOnce({stdout: 'line1\nline2\n'}); sedMock.mockReturnValueOnce({code: 0}); const filesToValidate = [ 'packages/react-native/package.json', @@ -160,9 +161,8 @@ describe('set-rn-version', () => { ]; const version = '0.81.0'; - setReactNativeVersion(version, 'release'); + setReactNativeVersion(version, null, 'release'); - expect(exitMock).toHaveBeenCalledWith(0); expect(echoMock).toHaveBeenNthCalledWith( 1, 'The tmp versioning folder is ./rn-set-version/', diff --git a/scripts/npm-utils.js b/scripts/npm-utils.js index 5586d7e3372952..d92de469014203 100644 --- a/scripts/npm-utils.js +++ b/scripts/npm-utils.js @@ -56,7 +56,35 @@ function pack(packagePath) { } } +/** + * `package` is an object form of package.json + * `dependencies` is a map of dependency to version string + * + * This replaces both dependencies and devDependencies in package.json + */ +function applyPackageVersions(originalPackageJson, packageVersions) { + const packageJson = {...originalPackageJson}; + + for (const name of Object.keys(packageVersions)) { + if ( + packageJson.dependencies != null && + packageJson.dependencies[name] != null + ) { + packageJson.dependencies[name] = packageVersions[name]; + } + + if ( + packageJson.devDependencies != null && + packageJson.devDependencies[name] != null + ) { + packageJson.devDependencies[name] = packageVersions[name]; + } + } + return packageJson; +} + module.exports = { + applyPackageVersions, getPackageVersionStrByTag, publishPackage, diffPackages, diff --git a/scripts/publish-npm.js b/scripts/publish-npm.js index e0223288059e34..b6cb1a12525a8c 100755 --- a/scripts/publish-npm.js +++ b/scripts/publish-npm.js @@ -9,7 +9,7 @@ 'use strict'; -const {exec, echo, exit} = require('shelljs'); +const {echo, exit} = require('shelljs'); const {parseVersion} = require('./version-utils'); const {getPackageVersionStrByTag, publishPackage} = require('./npm-utils'); const { @@ -18,6 +18,7 @@ const { isTaggedLatest, } = require('./scm-utils'); const getAndUpdateNightlies = require('./monorepo/get-and-update-nightlies'); +const setReactNativeVersion = require('./set-rn-version'); const { generateAndroidArtifacts, publishAndroidArtifactsToMaven, @@ -134,25 +135,24 @@ function getNpmInfo(buildType) { function publishNpm(buildType) { const {version, tag} = getNpmInfo(buildType); - // Set version number in various files (package.json, gradle.properties etc) - // For non-nightly, non-dry-run, CircleCI job `prepare_package_for_release` does this + // Here we update the react-native package and template package with the right versions + // For releases, CircleCI job `prepare_package_for_release` handles this if (buildType === 'nightly' || buildType === 'dry-run') { - // Sets the version for package/react-native - if ( - exec( - `node scripts/set-rn-version.js --to-version ${version} --build-type ${buildType}`, - ).code - ) { - echo(`Failed to set version number to ${version}`); + // Publish monorepo nightlies if there are updates, returns nightly versions for each + const monorepoNightlyVersions = + buildType === 'nightly' ? getAndUpdateNightlies(version) : null; + + try { + // Update the react-native and template packages with the react-native version + // and nightly versions of monorepo deps + setReactNativeVersion(version, monorepoNightlyVersions, buildType); + } catch (e) { + console.error(`Failed to set version number to ${version}`); + console.error(e); return exit(1); } } - // set and publish the relevant monorepo packages - if (buildType === 'nightly') { - getAndUpdateNightlies(version); - } - generateAndroidArtifacts(version); // Write version number to the build folder diff --git a/scripts/run-ci-e2e-tests.js b/scripts/run-ci-e2e-tests.js index 050577579e4895..56550cc262b822 100644 --- a/scripts/run-ci-e2e-tests.js +++ b/scripts/run-ci-e2e-tests.js @@ -64,7 +64,7 @@ try { } describe('Create react-native package'); - if (exec('node ./scripts/set-rn-version.js --version 1000.0.0').code) { + if (exec('node ./scripts/set-rn-version.js --to-version 1000.0.0').code) { echo('Failed to set version and update package.json ready for release'); exitCode = 1; throw Error(exitCode); diff --git a/scripts/set-rn-template-version.js b/scripts/set-rn-template-version.js deleted file mode 100755 index de8e00f745537f..00000000000000 --- a/scripts/set-rn-template-version.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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 - */ - -'use strict'; - -const fs = require('fs'); -const path = require('path'); - -const version = process.argv[2]; - -if (!version) { - console.error('Please provide a react-native version.'); - process.exit(1); -} - -const jsonPath = path.join( - __dirname, - '../packages/react-native/template/package.json', -); - -let templatePackageJson = require(jsonPath); -templatePackageJson.dependencies['react-native'] = version; -fs.writeFileSync( - jsonPath, - JSON.stringify(templatePackageJson, null, 2) + '\n', - 'utf-8', -); diff --git a/scripts/set-rn-version.js b/scripts/set-rn-version.js index a5f99213740ab0..ad8603471b4fe3 100755 --- a/scripts/set-rn-version.js +++ b/scripts/set-rn-version.js @@ -16,6 +16,8 @@ const {cat, echo, exec, exit, sed} = require('shelljs'); const yargs = require('yargs'); const {parseVersion, validateBuildType} = require('./version-utils'); const {saveFiles} = require('./scm-utils'); +const updateTemplatePackage = require('./update-template-package'); +const {applyPackageVersions} = require('./npm-utils'); /** * This script updates relevant React Native files with supplied version: @@ -31,13 +33,31 @@ if (require.main === module) { type: 'string', required: true, }) + .option('d', { + alias: 'dependency-versions', + type: 'string', + describe: + 'JSON string of package versions. Ex. "{"react-native":"0.64.1"}"', + default: null, + }) + .coerce('d', dependencyVersions => { + if (dependencyVersions == null) { + return null; + } + return JSON.parse(dependencyVersions); + }) .option('b', { alias: 'build-type', type: 'string', required: true, }).argv; - setReactNativeVersion(argv.toVersion, argv.buildType); + setReactNativeVersion( + argv.toVersion, + argv.dependencyVersions, + argv.buildType, + ); + exit(0); } function setSource({major, minor, patch, prerelease}) { @@ -108,8 +128,15 @@ function setGradle({version}) { } } -function setPackage({version}) { - const packageJson = JSON.parse(cat('packages/react-native/package.json')); +function setPackage({version}, dependencyVersions) { + const originalPackageJson = JSON.parse( + cat('packages/react-native/package.json'), + ); + const packageJson = + dependencyVersions != null + ? applyPackageVersions(originalPackageJson, dependencyVersions) + : originalPackageJson; + packageJson.version = version; fs.writeFileSync( @@ -119,15 +146,7 @@ function setPackage({version}) { ); } -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) { +function setReactNativeVersion(argVersion, dependencyVersions, buildType) { validateBuildType(buildType); const version = parseVersion(argVersion, buildType); @@ -145,8 +164,14 @@ function setReactNativeVersion(argVersion, buildType) { saveFiles(tmpVersioningFolder); setSource(version); - setPackage(version); - setTemplatePackage(version); + setPackage(version, dependencyVersions); + + const templateDependencyVersions = { + 'react-native': version.version, + ...(dependencyVersions != null ? dependencyVersions : {}), + }; + updateTemplatePackage(templateDependencyVersions); + setGradle(version); // Validate changes @@ -170,7 +195,7 @@ function setReactNativeVersion(argVersion, buildType) { echo(`These files already had version ${version.version} set.`); } - return exit(0); + return; } module.exports = setReactNativeVersion; diff --git a/scripts/test-e2e-local.js b/scripts/test-e2e-local.js index a6a891df5ac62b..d88349290a0d50 100644 --- a/scripts/test-e2e-local.js +++ b/scripts/test-e2e-local.js @@ -17,6 +17,7 @@ */ const {exec, exit, pushd, popd, pwd, cd, cp} = require('shelljs'); +const updateTemplatePackage = require('../scripts/update-template-package'); const yargs = require('yargs'); const fs = require('fs'); @@ -208,7 +209,9 @@ if (argv.target === 'RNTester') { ); const localNodeTGZPath = `${reactNativePackagePath}/react-native-${releaseVersion}.tgz`; - exec(`node scripts/set-rn-template-version.js "file:${localNodeTGZPath}"`); + updateTemplatePackage({ + 'react-native': `file:${localNodeTGZPath}`, + }); // create locally the node module exec('npm pack', {cwd: reactNativePackagePath}); diff --git a/scripts/update-template-package.js b/scripts/update-template-package.js new file mode 100755 index 00000000000000..9443707db562e8 --- /dev/null +++ b/scripts/update-template-package.js @@ -0,0 +1,54 @@ +/** + * 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 + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const {applyPackageVersions} = require('./npm-utils'); + +/** + * Updates the react-native template package.json with + * dependencies in `dependencyMap`. + * + * `dependencyMap` is a dict of package name to its version + * ex. {"react-native": "0.23.0", "other-dep": "nightly"} + */ +function updateTemplatePackage(dependencyMap) { + const jsonPath = path.join( + __dirname, + '../packages/react-native/template/package.json', + ); + const templatePackageJson = require(jsonPath); + + const updatedPackageJson = applyPackageVersions( + templatePackageJson, + dependencyMap, + ); + + fs.writeFileSync( + jsonPath, + JSON.stringify(updatedPackageJson, null, 2) + '\n', + 'utf-8', + ); +} + +if (require.main === module) { + const dependencyMapStr = process.argv[2]; + if (!dependencyMapStr) { + console.error( + 'Please provide a json string of package name and their version. Ex. \'{"packageName":"0.23.0"}\'', + ); + process.exit(1); + } + + updateTemplatePackage(JSON.parse(dependencyMapStr)); +} + +module.exports = updateTemplatePackage;