From d3ddec3bbd1409890ef2480b7f8d6987ba38f6ce Mon Sep 17 00:00:00 2001 From: Brett Higgins Date: Thu, 10 Sep 2020 22:09:40 -0400 Subject: [PATCH] feat: add native version bumpers Adds app version and build number bumpers for the native iOS/Android project configuration files (e.g. Info.plist, build.gradle). These are usable by expo bare workflow projects, but also react-native projects that don't use Expo, and even plain old iOS/Androd projects. Closes #9. --- README.md | 51 ++++++++ android/native/app-version.js | 1 + android/native/buildnum/code.js | 1 + android/native/buildnum/increment.js | 1 + android/native/buildnum/index.js | 1 + ios/native/app-version.js | 1 + ios/native/buildnum/code.js | 1 + ios/native/buildnum/increment.js | 1 + ios/native/buildnum/index.js | 1 + ios/native/buildnum/version.js | 1 + package-lock.json | 12 +- package.json | 2 + src/bumpers/native/android-app-version.ts | 11 ++ src/bumpers/native/buildnum/android-code.ts | 42 +++++++ .../native/buildnum/android-increment.ts | 24 ++++ src/bumpers/native/buildnum/ios-code.ts | 42 +++++++ src/bumpers/native/buildnum/ios-increment.ts | 24 ++++ src/bumpers/native/buildnum/ios-version.ts | 11 ++ src/bumpers/native/helpers.ts | 117 ++++++++++++++++++ src/bumpers/native/ios-app-version.ts | 11 ++ src/versions.ts | 8 +- .../native/android-app-version.test.ts | 38 ++++++ .../native/buildnum/android-code.test.ts | 54 ++++++++ .../native/buildnum/android-increment.test.ts | 40 ++++++ test/bumpers/native/buildnum/ios-code.test.ts | 64 ++++++++++ .../native/buildnum/ios-increment.test.ts | 46 +++++++ .../native/buildnum/ios-version.test.ts | 48 +++++++ test/bumpers/native/ios-app-version.test.ts | 45 +++++++ test/stub.ts | 50 ++++++++ test/versions/get-version-code.test.ts | 12 +- 30 files changed, 756 insertions(+), 5 deletions(-) create mode 100644 android/native/app-version.js create mode 100644 android/native/buildnum/code.js create mode 100644 android/native/buildnum/increment.js create mode 100644 android/native/buildnum/index.js create mode 100644 ios/native/app-version.js create mode 100644 ios/native/buildnum/code.js create mode 100644 ios/native/buildnum/increment.js create mode 100644 ios/native/buildnum/index.js create mode 100644 ios/native/buildnum/version.js create mode 100644 src/bumpers/native/android-app-version.ts create mode 100644 src/bumpers/native/buildnum/android-code.ts create mode 100644 src/bumpers/native/buildnum/android-increment.ts create mode 100644 src/bumpers/native/buildnum/ios-code.ts create mode 100644 src/bumpers/native/buildnum/ios-increment.ts create mode 100644 src/bumpers/native/buildnum/ios-version.ts create mode 100644 src/bumpers/native/helpers.ts create mode 100644 src/bumpers/native/ios-app-version.ts create mode 100644 test/bumpers/native/android-app-version.test.ts create mode 100644 test/bumpers/native/buildnum/android-code.test.ts create mode 100644 test/bumpers/native/buildnum/android-increment.test.ts create mode 100644 test/bumpers/native/buildnum/ios-code.test.ts create mode 100644 test/bumpers/native/buildnum/ios-increment.test.ts create mode 100644 test/bumpers/native/buildnum/ios-version.test.ts create mode 100644 test/bumpers/native/ios-app-version.test.ts diff --git a/README.md b/README.md index f2f1104..4de7a5a 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,34 @@ module.exports = { }; ``` +If you're using the [bare workflow][link-bare-workflow], you'll need a couple +more bumpers to keep your native project config files in sync: + +```js +// .versionrc.js +const sdkVersion = '37.0.0'; // or pull from app.json + +module.exports = [ + // ... + { + filename: 'ios//Info.plist', + updater: require.resolve('standard-version-expo/ios/native/app-version'), + }, + { + filename: 'ios//Info.plist', + updater: require.resolve('standard-version-expo/ios/native/buildnum/increment'), + }, + { + filename: 'android/app/build.gradle', + updater: require.resolve('standard-version-expo/android/native/app-version'), + }, + { + filename: 'android/app/build.gradle', + updater: require.resolve('standard-version-expo/android/native/buildnum/code')(sdkVersion), + }, +]; +``` + To test if your configuration works as expected, you can run standard version in dry mode. This shows you what will happen, without actually applying the versions and tags. @@ -87,6 +115,28 @@ updater | example | description `ios/increment` | `9` | Replace `expo.ios.buildNumber` with an incremental version. `ios/version` | `3.2.1` | Replace `expo.ios.buildNumber` with the exact calculated semver. (**recommended**) +And for the native build config files: + +updater | example | file path | description +--- | --- | --- | --- +`native/ios/app-version` | `3.2.1` | `ios//Info.plist` | Replace `CFBundleShortVersionString` with the exact calculated semver. +`native/ios/buildnum/code` | `36030201` | `ios//Info.plist` | Replace `CFBundleVersion` with the [method described by Maxi Rosson][link-version-code]. +`native/ios/buildnum/increment` | `8` | `ios//Info.plist` | Replace `CFBundleVersion` with an incremental version. +`native/ios/buildnum/version` | `3.2.1` | `ios//Info.plist` | Replace `CFBundleVersion` with the exact calculated semver. (**recommended**) +`native/android/app-version` | `3.2.1` | `android/app/build.gradle` | Replace `versionName` with the exact calculated semver. +`native/android/buildnum/code` | `36030201` | `android/app/build.gradle` | Replace `versionCode` with the [method described by Maxi Rosson][link-version-code]. (**recommended**) +`native/android/buildnum/increment` | `8` | `android/app/build.gradle` | Replace `versionCode` with an incremental version. + +Note that the `native/{ios,android}/buildnum/code` bumpers are only supported +in `.versionrc.js` file, not in `.versionrc` or `.versionrc.json` files. +Since a bumper only operates on one file, the Expo manifest is unavailable to +the bumper when it's operating on a native build config file. Because of this, +you must provide the Expo SDK version via javascript (see example above). + +However, this means that you can also use these bumpers with non-Expo React +Native projects, and even plain Android projects, simply by supplying the +minimum Android API level rather than the Expo SDK version. + ### Version code Semver is one of the most popular versioning methods; it generates a string with a syntax that even humans can read. @@ -108,3 +158,4 @@ It's a deterministic solution that removes some of the ambiguity of incremental [link-expo-version]: https://docs.expo.io/versions/latest/workflow/configuration#version [link-standard-version]: https://github.com/conventional-changelog/standard-version#configuration [link-version-code]: https://medium.com/@maxirosson/versioning-android-apps-d6ec171cfd82 +[link-bare-workflow]: https://docs.expo.io/introduction/managed-vs-bare/ diff --git a/android/native/app-version.js b/android/native/app-version.js new file mode 100644 index 0000000..b00aa2e --- /dev/null +++ b/android/native/app-version.js @@ -0,0 +1 @@ +module.exports = require('../../build/bumpers/native/android-app-version'); diff --git a/android/native/buildnum/code.js b/android/native/buildnum/code.js new file mode 100644 index 0000000..5596191 --- /dev/null +++ b/android/native/buildnum/code.js @@ -0,0 +1 @@ +module.exports = require('../../build/bumpers/native/buildnum/android-code'); diff --git a/android/native/buildnum/increment.js b/android/native/buildnum/increment.js new file mode 100644 index 0000000..e9cd67b --- /dev/null +++ b/android/native/buildnum/increment.js @@ -0,0 +1 @@ +module.exports = require('../../build/bumpers/native/buildnum/android-increment'); diff --git a/android/native/buildnum/index.js b/android/native/buildnum/index.js new file mode 100644 index 0000000..5596191 --- /dev/null +++ b/android/native/buildnum/index.js @@ -0,0 +1 @@ +module.exports = require('../../build/bumpers/native/buildnum/android-code'); diff --git a/ios/native/app-version.js b/ios/native/app-version.js new file mode 100644 index 0000000..2319b56 --- /dev/null +++ b/ios/native/app-version.js @@ -0,0 +1 @@ +module.exports = require('../../build/bumpers/native/ios-app-version'); diff --git a/ios/native/buildnum/code.js b/ios/native/buildnum/code.js new file mode 100644 index 0000000..b242040 --- /dev/null +++ b/ios/native/buildnum/code.js @@ -0,0 +1 @@ +module.exports = require('../../build/bumpers/native/buildnum/ios-code'); diff --git a/ios/native/buildnum/increment.js b/ios/native/buildnum/increment.js new file mode 100644 index 0000000..907fca3 --- /dev/null +++ b/ios/native/buildnum/increment.js @@ -0,0 +1 @@ +module.exports = require('../../build/bumpers/native/buildnum/ios-increment'); diff --git a/ios/native/buildnum/index.js b/ios/native/buildnum/index.js new file mode 100644 index 0000000..c6dfe29 --- /dev/null +++ b/ios/native/buildnum/index.js @@ -0,0 +1 @@ +module.exports = require('../../build/bumpers/native/buildnum/ios-version'); diff --git a/ios/native/buildnum/version.js b/ios/native/buildnum/version.js new file mode 100644 index 0000000..c6dfe29 --- /dev/null +++ b/ios/native/buildnum/version.js @@ -0,0 +1 @@ +module.exports = require('../../build/bumpers/native/buildnum/ios-version'); diff --git a/package-lock.json b/package-lock.json index 2e482bd..7fe2f20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1362,8 +1362,16 @@ "@types/node": { "version": "13.13.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.0.tgz", - "integrity": "sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A==", - "dev": true + "integrity": "sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A==" + }, + "@types/plist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz", + "integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==", + "requires": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } }, "@types/prettier": { "version": "1.19.1", diff --git a/package.json b/package.json index 1cd88c2..3cb2af5 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,11 @@ "dependencies": { "@expo/config": "^3.1.2", "@expo/json-file": "^8.2.10", + "@types/plist": "^3.0.2", "detect-indent": "^6.0.0", "detect-newline": "^3.1.0", "globby": "^11.0.0", + "plist": "^3.0.1", "semver": "^7.3.2" }, "devDependencies": { diff --git a/src/bumpers/native/android-app-version.ts b/src/bumpers/native/android-app-version.ts new file mode 100644 index 0000000..1d8ffd5 --- /dev/null +++ b/src/bumpers/native/android-app-version.ts @@ -0,0 +1,11 @@ +import { androidAppVersionReader, androidAppVersionWriter } from './helpers'; + +/** + * Read the app version from the `versionName` build.gradle property. + */ +export const readVersion = androidAppVersionReader; + +/** + * Write the app version to the `versionName` build.gradle property. + */ +export const writeVersion = androidAppVersionWriter; diff --git a/src/bumpers/native/buildnum/android-code.ts b/src/bumpers/native/buildnum/android-code.ts new file mode 100644 index 0000000..dece6a0 --- /dev/null +++ b/src/bumpers/native/buildnum/android-code.ts @@ -0,0 +1,42 @@ +import { androidBuildnumReader, androidBuildnumWriter } from '../helpers'; +import { getVersionCodeFromSdkVersion } from '../../../versions'; + +/** + * Since a standard-version bumper only receives the contents of a single file, + * we add a layer of indirection here and ask the user to supply the sdkVersion + * directly. Note that they can choose to pull this from app.json, or even supply + * the Android min SDK version if they're not using Expo. + * + * Configuration example in .versionrc.js: + * + * const sdkVersion = '37.0.0'; // or pull from app.json + * module.exports = [ + * ... + * { + * filename: 'android/app/build.gradle', + * updater: require.resolve('standard-version-expo/android/native/code')(sdkVersion), + * }, + * ... + * ]; + * + * This does add the requirement that they use .versionrc.js, not the other formats. + */ +export default (sdkVersion: string) => ({ + /** + * Read the build code from the `versionCode` property. + */ + readVersion: androidBuildnumReader, + + /** + * Write the manifest version to the `versionCode` property. + * This uses the Android version code approach of Maxi Rosson. + * + * @see https://medium.com/@maxirosson/versioning-android-apps-d6ec171cfd82 + */ + writeVersion: (contents: string, version: string) => androidBuildnumWriter( + contents, + String( + getVersionCodeFromSdkVersion(sdkVersion, version), + ), + ), +}); diff --git a/src/bumpers/native/buildnum/android-increment.ts b/src/bumpers/native/buildnum/android-increment.ts new file mode 100644 index 0000000..98ca833 --- /dev/null +++ b/src/bumpers/native/buildnum/android-increment.ts @@ -0,0 +1,24 @@ +import { androidBuildnumReader, androidBuildnumWriter } from '../helpers'; +import { VersionWriter } from '../../../types'; + +/** + * Read the buildnum stored at versionCode in the build.gradle. + */ +export const readVersion = androidBuildnumReader; + +/** + * Increment the buildnum stored at versionCode in the build.gradle. + * This ignores the provided version. + */ +export const writeVersion: VersionWriter = (contents, _version) => { + const buildNumStr = androidBuildnumReader(contents); + const buildNumber = buildNumStr != '' + ? Number(buildNumStr) + : 0; + + if (Number.isNaN(buildNumber)) { + throw new Error('Could not parse number from `versionCode`.'); + } + + return androidBuildnumWriter(contents, String(buildNumber + 1)); +}; diff --git a/src/bumpers/native/buildnum/ios-code.ts b/src/bumpers/native/buildnum/ios-code.ts new file mode 100644 index 0000000..c764c1d --- /dev/null +++ b/src/bumpers/native/buildnum/ios-code.ts @@ -0,0 +1,42 @@ +import { iosBuildnumReader, iosBuildnumWriter } from '../helpers'; +import { getVersionCodeFromSdkVersion } from '../../../versions'; + +/** + * Since a standard-version bumper only receives the contents of a single file, + * we add a layer of indirection here and ask the user to supply the sdkVersion + * directly. Note that they can choose to pull this from app.json, or even supply + * the Android min SDK version if they're not using Expo. + * + * Configuration example in .versionrc.js: + * + * const sdkVersion = '37.0.0'; // or pull from app.json + * module.exports = [ + * ... + * { + * filename: 'ios/MyApp/Info.plist', + * updater: require.resolve('standard-version-expo/ios/native/code')(sdkVersion), + * }, + * ... + * ]; + * + * This does add the requirement that they use .versionrc.js, not the other formats. + */ +export default (sdkVersion: string) => ({ + /** + * Read the build code from the `CFBundleVersion` property. + */ + readVersion: iosBuildnumReader, + + /** + * Write the manifest version to the `CFBundleVersion` property. + * This uses the Android version code approach of Maxi Rosson. + * + * @see https://medium.com/@maxirosson/versioning-android-apps-d6ec171cfd82 + */ + writeVersion: (contents: string, version: string) => iosBuildnumWriter( + contents, + String( + getVersionCodeFromSdkVersion(sdkVersion, version), + ), + ), +}); diff --git a/src/bumpers/native/buildnum/ios-increment.ts b/src/bumpers/native/buildnum/ios-increment.ts new file mode 100644 index 0000000..0b59d93 --- /dev/null +++ b/src/bumpers/native/buildnum/ios-increment.ts @@ -0,0 +1,24 @@ +import { iosBuildnumReader, iosBuildnumWriter } from '../helpers'; +import { VersionWriter } from '../../../types'; + +/** + * Read the buildnum stored at CFBundleVersion in the Info.plist. + */ +export const readVersion = iosBuildnumReader; + +/** + * Increment the buildnum stored at CFBundleVersion in the Info.plist. + * This ignores the provided version. + */ +export const writeVersion: VersionWriter = (contents, _version) => { + const buildNumStr = iosBuildnumReader(contents); + const buildNumber = buildNumStr != '' + ? Number(buildNumStr) + : 0; + + if (Number.isNaN(buildNumber)) { + throw new Error('Could not parse number from `CFBundleVersion`.'); + } + + return iosBuildnumWriter(contents, String(buildNumber + 1)); +}; diff --git a/src/bumpers/native/buildnum/ios-version.ts b/src/bumpers/native/buildnum/ios-version.ts new file mode 100644 index 0000000..d132eeb --- /dev/null +++ b/src/bumpers/native/buildnum/ios-version.ts @@ -0,0 +1,11 @@ +import { iosBuildnumReader, iosBuildnumWriter } from '../helpers'; + +/** + * Read the build version stored at CFBundleVersion in the Info.plist. + */ +export const readVersion = iosBuildnumReader; + +/** + * Write the manifest version at CFBundleVersion in the Info.plist. + */ +export const writeVersion = iosBuildnumWriter; diff --git a/src/bumpers/native/helpers.ts b/src/bumpers/native/helpers.ts new file mode 100644 index 0000000..af20011 --- /dev/null +++ b/src/bumpers/native/helpers.ts @@ -0,0 +1,117 @@ +import os from 'os'; + +import plist, { PlistObject } from 'plist'; + +import { VersionReader, VersionWriter } from '../../types'; + +const androidAppVersionRegex = /^(.+versionName +["']?)([^"']+)(["']?.*)$/; +const androidBuildnumRegex = /^(.+versionCode +["']?)([0-9]+)(["']?.*)$/; + +const replaceMatchingLines = (contents: string, rx: RegExp, version: string): string => { + // it's a PITA to make sure we insert it in the right place, + // and it's always there in the generated android project, + // so if we don't find it, throw an error. + if (!findMatchingLine(contents, rx)) { + let field; + if (rx === androidAppVersionRegex) { + field = 'versionName'; + } else if (rx === androidBuildnumRegex) { + field = 'versionCode'; + } else { + throw new Error('NOTREACHED'); + } + throw new Error(`Could not find ${field} in build.gradle`) + } + + return contents.split(os.EOL) + .map(line => line.replace(rx, `$1${version}$3`)) + .join(os.EOL) +}; + +const findMatchingLine = (contents: string, rx: RegExp): string => { + for (const line of contents.split(os.EOL)) { + const match = line.match(rx); + if (match) { + return match[2]; + } + } + return ''; +} + +/** + * The default android app version reader. + * It reads the value from `android/app/build.gradle` and returns it as string. + */ +export const androidAppVersionReader: VersionReader = (contents) => ( + findMatchingLine(contents, androidAppVersionRegex) +); + +/** + * The default android buildnum reader. + * It reads the value from `android/app/build.gradle` and returns it as string. + */ +export const androidBuildnumReader: VersionReader = (contents) => ( + findMatchingLine(contents, androidBuildnumRegex) +); + + +/** + * The default android app version writer. + * It replaces the value in the `android/app/build.gradle` contents and + * returns the new contents as string. + */ +export const androidAppVersionWriter: VersionWriter = (contents, version) => ( + replaceMatchingLines(contents, androidAppVersionRegex, version) +); + +/** + * The default android buildnum writer. + * It replaces the value in the `android/app/build.gradle` contents and + * returns the new contents as string. + */ +export const androidBuildnumWriter: VersionWriter = (contents, version) => ( + replaceMatchingLines(contents, androidBuildnumRegex, version) +); + +const iosReadVersion = (contents: string, key: string): string => ( + (plist.parse(contents) as PlistObject)[key] as string || '' +) + +/** + * The default ios app version reader. + * It reads the value from `Info.plist` and returns it as string. + */ +export const iosAppVersionReader: VersionReader = (contents) => ( + iosReadVersion(contents, 'CFBundleShortVersionString') +); + +/** + * The default ios buildnum reader. + * It reads the value from `Info.plist` and returns it as string. + */ +export const iosBuildnumReader: VersionReader = (contents) => ( + iosReadVersion(contents, 'CFBundleVersion') +); + +const iosWriteVersion = (contents: string, key: string, value: string): string => ( + plist.build({ + ...(plist.parse(contents) as PlistObject), + [key]: value, + }) +) + +/** + * The default ios app version writer. + * It replaces the value in the `Info.plist` contents and returns the new contents as string. + */ +export const iosAppVersionWriter: VersionWriter = (contents, version) => ( + iosWriteVersion(contents, 'CFBundleShortVersionString', version) +); + +/** + * The default ios buildnum writer. + * It replaces the value in the `Info.plist` contents and returns the new contents as string. + */ +export const iosBuildnumWriter: VersionWriter = (contents, version) => ( + iosWriteVersion(contents, 'CFBundleVersion', version) +); diff --git a/src/bumpers/native/ios-app-version.ts b/src/bumpers/native/ios-app-version.ts new file mode 100644 index 0000000..8e76300 --- /dev/null +++ b/src/bumpers/native/ios-app-version.ts @@ -0,0 +1,11 @@ +import { iosAppVersionReader, iosAppVersionWriter } from './helpers'; + +/** + * Read the app version from the `CFBundleShortVersionString` Info.plist property. + */ +export const readVersion = iosAppVersionReader; + +/** + * Write the app version to the `CFBundleShortVersionString` Info.plist property. + */ +export const writeVersion = iosAppVersionWriter; diff --git a/src/versions.ts b/src/versions.ts index d910e09..6037256 100644 --- a/src/versions.ts +++ b/src/versions.ts @@ -8,8 +8,12 @@ import { coerce as semver } from 'semver'; * @see https://medium.com/@maxirosson/versioning-android-apps-d6ec171cfd82 */ export function getVersionCode(manifest: AppJSONConfig, version: string): number { - const sdk = getExpoSDKVersion(process.cwd(), manifest.expo); - const expo = semver(sdk); + const sdkVersion = getExpoSDKVersion(process.cwd(), manifest.expo); + return getVersionCodeFromSdkVersion(sdkVersion, version); +} + +export function getVersionCodeFromSdkVersion(sdkVersion: string, version: string): number { + const expo = semver(sdkVersion); const target = semver(version); if (!expo) { diff --git a/test/bumpers/native/android-app-version.test.ts b/test/bumpers/native/android-app-version.test.ts new file mode 100644 index 0000000..059ebe6 --- /dev/null +++ b/test/bumpers/native/android-app-version.test.ts @@ -0,0 +1,38 @@ +import * as stub from '../../stub'; +import { readVersion, writeVersion } from '../../../src/bumpers/native/android-app-version'; + +describe('readVersion', () => { + it('returns android app version from build.gradle', () => { + const version = '4.2.1'; + const buildGradle = stub.androidBuildGradle(version); + + expect(readVersion(buildGradle)).toBe(version); + }); + + it('returns empty string by default', () => { + const buildGradle = stub.androidBuildGradle(); + const buildGradleNoAppVersion = buildGradle.replace(/\t+versionName +[0-9.'"]+/m, ''); + + expect(readVersion(buildGradleNoAppVersion)).toBe(''); + }); +}); + +describe('writeVersion', () => { + it('returns build.gradle with modified android app version', () => { + const newVersion = '3.2.1'; + const buildGradle = stub.androidBuildGradle(); + expect(readVersion(buildGradle)).not.toBe(newVersion); + + const modified = writeVersion(buildGradle, newVersion); + + expect(readVersion(modified)).toBe(newVersion); + }); + + it('throws if android app version is missing', () => { + const version = '1.2.3'; + const buildGradle = stub.androidBuildGradle(); + const buildGradleNoAppVersion = buildGradle.replace(/\t+versionName +[0-9.'"]+/m, ''); + + expect(() => writeVersion(buildGradleNoAppVersion, version)).toThrow(); + }); +}); diff --git a/test/bumpers/native/buildnum/android-code.test.ts b/test/bumpers/native/buildnum/android-code.test.ts new file mode 100644 index 0000000..d918955 --- /dev/null +++ b/test/bumpers/native/buildnum/android-code.test.ts @@ -0,0 +1,54 @@ +import * as stub from '../../../stub'; +import androidCodeBumper from '../../../../src/bumpers/native/buildnum/android-code'; +import { getVersionCodeFromSdkVersion } from '../../../../src/versions'; + +describe('readVersion', () => { + const sdkVersion = '37.0.0'; + const { readVersion } = androidCodeBumper(sdkVersion); + + it('returns android build number from build.gradle', () => { + const version = '1.0.0'; + const buildnum = '370010000'; + const buildGradle = stub.androidBuildGradle(version, buildnum); + + expect(readVersion(buildGradle)).toBe( + String(getVersionCodeFromSdkVersion(sdkVersion, version)) + ); + }); + + it('returns empty string by default', () => { + const buildGradle = stub.androidBuildGradle(); + expect(readVersion(buildGradle)).not.toBe(''); + + const buildGradleNoBuildnum = buildGradle.replace(/\t+versionCode +[0-9]+/m, ''); + + expect(readVersion(buildGradleNoBuildnum)).toBe(''); + }); +}); + +describe('writeVersion', () => { + const sdkVersion = '37.0.0'; + const { readVersion, writeVersion } = androidCodeBumper(sdkVersion); + + it('returns build.gradle with modified android build number', () => { + const version = '3.2.1'; + const expectedBuildnum = '370030201'; + const buildGradle = stub.androidBuildGradle(version, '123'); + expect(readVersion(buildGradle)).not.toBe(expectedBuildnum); + + const modified = writeVersion(buildGradle, version); + + expect(readVersion(modified)).toBe(expectedBuildnum); + }); + + it('throws if android build number is missing', () => { + const oldBuildnum = '370030201'; + const oldVersion = '3.2.1'; + const version = '1.2.3'; + + const buildGradle = stub.androidBuildGradle(oldVersion, oldBuildnum); + const buildGradleNoBuildnum = buildGradle.replace(/\t+versionCode +[0-9]+/m, ''); + + expect(() => writeVersion(buildGradleNoBuildnum, version)).toThrow(); + }); +}); diff --git a/test/bumpers/native/buildnum/android-increment.test.ts b/test/bumpers/native/buildnum/android-increment.test.ts new file mode 100644 index 0000000..12aed40 --- /dev/null +++ b/test/bumpers/native/buildnum/android-increment.test.ts @@ -0,0 +1,40 @@ +import * as stub from '../../../stub'; +import { readVersion, writeVersion } from '../../../../src/bumpers/native/buildnum/android-increment'; + +describe('readVersion', () => { + it('returns android build number from build.gradle', () => { + const buildnum = '5'; + const buildGradle = stub.androidBuildGradle(undefined, buildnum); + + expect(readVersion(buildGradle)).toBe(buildnum); + }); + + it('returns empty string by default', () => { + const buildGradle = stub.androidBuildGradle(); + expect(readVersion(buildGradle)).not.toBe(''); + + const buildGradleNoBuildnum = buildGradle.replace(/\t+versionCode +[0-9]+/m, ''); + + expect(readVersion(buildGradleNoBuildnum)).toBe(''); + }); +}); + +describe('writeVersion', () => { + it('returns build.gradle with modified android build number', () => { + const oldBuildNum = '4'; + const newBuildNum = '5'; + const buildGradle = stub.androidBuildGradle(undefined, oldBuildNum); + expect(readVersion(buildGradle)).not.toBe(newBuildNum); + + const modified = writeVersion(buildGradle, 'ignored'); + + expect(readVersion(modified)).toBe(newBuildNum); + }); + + it('throws if android build number is missing', () => { + const buildGradle = stub.androidBuildGradle(); + const buildGradleNoBuildnum = buildGradle.replace(/\t+versionCode +[0-9]+/m, ''); + + expect(() => writeVersion(buildGradleNoBuildnum, 'ignored')).toThrow(); + }); +}); diff --git a/test/bumpers/native/buildnum/ios-code.test.ts b/test/bumpers/native/buildnum/ios-code.test.ts new file mode 100644 index 0000000..bb43838 --- /dev/null +++ b/test/bumpers/native/buildnum/ios-code.test.ts @@ -0,0 +1,64 @@ +import plist, { PlistObject } from 'plist'; + +import * as stub from '../../../stub'; +import iosCodeBumper from '../../../../src/bumpers/native/buildnum/ios-code'; +import { getVersionCodeFromSdkVersion } from '../../../../src/versions'; + +describe('readVersion', () => { + const sdkVersion = '37.0.0'; + const { readVersion } = iosCodeBumper(sdkVersion); + + it('returns ios build number from Info.plist', () => { + const version = '1.0.0'; + const buildnum = '370010000'; + const infoPlist = stub.iosInfoPlist(version, buildnum); + + expect(readVersion(infoPlist)).toBe( + String(getVersionCodeFromSdkVersion(sdkVersion, version)) + ); + }); + + it('returns empty string by default', () => { + const infoPlistStr = stub.iosInfoPlist(); + const infoPlist: PlistObject = plist.parse(infoPlistStr) as PlistObject; + expect(readVersion(infoPlistStr)).not.toBe(''); + + const writable = { ...infoPlist }; + delete writable.CFBundleVersion; + + expect(readVersion(plist.build(writable))).toBe(''); + }); +}); + +describe('writeVersion', () => { + const sdkVersion = '37.0.0'; + const { readVersion, writeVersion } = iosCodeBumper(sdkVersion); + + it('returns Info.plist with modified ios build number', () => { + const version = '3.2.1'; + const expectedBuildnum = '370030201'; + const infoPlistStr = stub.iosInfoPlist(version, '123'); + expect(readVersion(infoPlistStr)).not.toBe(expectedBuildnum); + + const modified = writeVersion(infoPlistStr, version); + + expect(readVersion(modified)).toBe(expectedBuildnum); + }); + + it('returns Info.plist with added ios build number', () => { + const oldBuildnum = '370030201'; + const oldVersion = '3.2.1'; + const version = '1.2.3'; + const newBuildnum = '370010203'; + + const infoPlistStr = stub.iosInfoPlist(oldVersion, oldBuildnum); + const infoPlist: PlistObject = plist.parse(infoPlistStr) as PlistObject; + + const writable = { ...infoPlist }; + delete writable.CFBundleVersion; + + const modified = writeVersion(plist.build(writable), version); + + expect(readVersion(modified)).toBe(newBuildnum); + }); +}); diff --git a/test/bumpers/native/buildnum/ios-increment.test.ts b/test/bumpers/native/buildnum/ios-increment.test.ts new file mode 100644 index 0000000..6419c1d --- /dev/null +++ b/test/bumpers/native/buildnum/ios-increment.test.ts @@ -0,0 +1,46 @@ +import plist, { PlistObject } from 'plist'; + +import * as stub from '../../../stub'; +import { readVersion, writeVersion } from '../../../../src/bumpers/native/buildnum/ios-increment'; + +describe('readVersion', () => { + it('returns ios build number from Info.plist', () => { + const buildnum = '5'; + const infoPlist = stub.iosInfoPlist(undefined, buildnum); + + expect(readVersion(infoPlist)).toBe(buildnum); + }); + + it('returns empty string by default', () => { + const infoPlistStr = stub.iosInfoPlist(); + const infoPlist: PlistObject = plist.parse(infoPlistStr) as PlistObject; + expect(readVersion(infoPlistStr)).not.toBe(''); + + const writable = { ...infoPlist }; + delete writable.CFBundleVersion; + + expect(readVersion(plist.build(writable))).toBe(''); + }); +}); + +describe('writeVersion', () => { + it('returns Info.plist with modified ios build number', () => { + const buildnum = '5'; + const infoPlist = stub.iosInfoPlist(undefined, buildnum); + + const modified = writeVersion(infoPlist, 'ignored'); + expect(readVersion(modified)).toBe('6'); + }); + + it('returns Info.plist with added ios build number', () => { + const infoPlistStr = stub.iosInfoPlist(); + const infoPlist: PlistObject = plist.parse(infoPlistStr) as PlistObject; + + const writable = { ...infoPlist }; + delete writable.CFBundleVersion; + + const modified = writeVersion(plist.build(writable), 'ignored'); + + expect(readVersion(modified)).toBe('1'); + }); +}); diff --git a/test/bumpers/native/buildnum/ios-version.test.ts b/test/bumpers/native/buildnum/ios-version.test.ts new file mode 100644 index 0000000..87e1d03 --- /dev/null +++ b/test/bumpers/native/buildnum/ios-version.test.ts @@ -0,0 +1,48 @@ +import plist, { PlistObject } from 'plist'; + +import * as stub from '../../../stub'; +import { readVersion, writeVersion } from '../../../../src/bumpers/native/buildnum/ios-version'; + +describe('readVersion', () => { + it('returns ios build number from Info.plist', () => { + const buildnum = '3.2.1'; + const infoPlist = stub.iosInfoPlist(undefined, buildnum); + + expect(readVersion(infoPlist)).toBe(buildnum); + }); + + it('returns empty string by default', () => { + const infoPlistStr = stub.iosInfoPlist(); + const infoPlist: PlistObject = plist.parse(infoPlistStr) as PlistObject; + expect(readVersion(infoPlistStr)).not.toBe(''); + + const writable = { ...infoPlist }; + delete writable.CFBundleVersion; + + expect(readVersion(plist.build(writable))).toBe(''); + }); +}); + +describe('writeVersion', () => { + it('returns Info.plist with modified ios build number', () => { + const buildnum = '1.2.3'; + const infoPlist = stub.iosInfoPlist(undefined, buildnum); + + const newBuildnum = '1.2.4'; + const modified = writeVersion(infoPlist, newBuildnum); + expect(readVersion(modified)).toBe(newBuildnum); + }); + + it('returns Info.plist with added ios build number', () => { + const infoPlistStr = stub.iosInfoPlist(); + const infoPlist: PlistObject = plist.parse(infoPlistStr) as PlistObject; + + const writable = { ...infoPlist }; + delete writable.CFBundleVersion; + + const newBuildnum = '1.2.3'; + const modified = writeVersion(plist.build(writable), newBuildnum); + + expect(readVersion(modified)).toBe(newBuildnum); + }); +}); diff --git a/test/bumpers/native/ios-app-version.test.ts b/test/bumpers/native/ios-app-version.test.ts new file mode 100644 index 0000000..8906988 --- /dev/null +++ b/test/bumpers/native/ios-app-version.test.ts @@ -0,0 +1,45 @@ +import plist, { PlistObject } from 'plist'; + +import * as stub from '../../stub'; +import { readVersion, writeVersion } from '../../../src/bumpers/native/ios-app-version'; + +describe('readVersion', () => { + it('returns ios app version from Info.plist', () => { + const version = '4.2.1'; + const infoPlistStr = stub.iosInfoPlist(version); + + expect(readVersion(infoPlistStr)).toBe(version); + }); + + it('returns empty string by default', () => { + const infoPlist: PlistObject = plist.parse(stub.iosInfoPlist()) as PlistObject; + const writable = { ...infoPlist }; + delete writable.CFBundleShortVersionString; + + expect(readVersion(plist.build(writable))).toBe(''); + }); +}); + +describe('writeVersion', () => { + it('returns Info.plist with modified ios app version', () => { + const newVersion = '3.2.1'; + const infoPlistStr = stub.iosInfoPlist(); + expect(readVersion(infoPlistStr)).not.toBe(newVersion); + + const modified = writeVersion(infoPlistStr, newVersion); + + expect(readVersion(modified)).toBe(newVersion); + }); + + it('returns Info.plist with added ios app version', () => { + const version = '1.2.3'; + const infoPlist = plist.parse(stub.iosInfoPlist()) as PlistObject; + const writable = { ...infoPlist }; + delete writable.CFBundleShortVersionString; + expect(readVersion(plist.build(writable))).toBe(''); + + const modified = writeVersion(plist.build(writable), version); + + expect(readVersion(modified)).toBe(version); + }); +}); diff --git a/test/stub.ts b/test/stub.ts index f0be4d4..c4081d7 100644 --- a/test/stub.ts +++ b/test/stub.ts @@ -29,3 +29,53 @@ export const manifestRaw = `{ * It's a function that returns new objects to avoid mutating the original data. */ export const manifest = () => JSON.parse(manifestRaw) as AppJSONConfig; + +/** + * Minimal Info.plist from an iOS project, for testing purposes. + */ +export const iosInfoPlistRaw = ` + + + + + CFBundleShortVersionString + VERSION + CFBundleVersion + BUILDNUM + + +`; + +/** + * Minimal app/build.gradle file from an Android project, for testing purposes. + */ +export const androidBuildGradleRaw = ` +android { + defaultConfig { + versionName "VERSION" + versionCode BUILDNUM + } +} +`; + +type StubGenerator = (version?: string, buildnum?: string) => string; + +const makeStubGenerator = (content: string): StubGenerator => ( + (version = '1.0.0', buildnum = '42') => ( + content + .replace('VERSION', version) + .replace('BUILDNUM', buildnum) + ) +); + +/** + * The example Info.plist, but cloned, for testing purposes. + * It's a function that returns new objects to avoid mutating the original data. + */ +export const iosInfoPlist: StubGenerator = makeStubGenerator(iosInfoPlistRaw); + +/** + * The example app/build.gradle, but cloned, for testing purposes. + * It's a function that returns new objects to avoid mutating the original data. + */ +export const androidBuildGradle: StubGenerator = makeStubGenerator(androidBuildGradleRaw); diff --git a/test/versions/get-version-code.test.ts b/test/versions/get-version-code.test.ts index f30d666..038a88f 100644 --- a/test/versions/get-version-code.test.ts +++ b/test/versions/get-version-code.test.ts @@ -1,6 +1,6 @@ import path from 'path'; import * as stub from '../stub'; -import { getVersionCode } from '../../src/versions'; +import { getVersionCode, getVersionCodeFromSdkVersion } from '../../src/versions'; describe('getVersionCode', () => { it('throws when `expo.sdkVersion` is missing', () => { @@ -49,3 +49,13 @@ describe('getVersionCode', () => { expect(getVersionCode(manifest, '3.2.1')).toBe(370030201); }); }); + +describe('getVersionCodeFromSdkVersion', () => { + it('returns 370030201 for sdk 37 and version 3.2.1', () => { + expect(getVersionCodeFromSdkVersion('37.0.0', '3.2.1')).toBe(370030201); + }); + + it('returns 280030201 for API level 28 and version 3.2.1', () => { + expect(getVersionCodeFromSdkVersion('28', '3.2.1')).toBe(280030201); + }); +});