Skip to content

Commit

Permalink
feat: add native version bumpers
Browse files Browse the repository at this point in the history
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/Android projects.

Closes expo-community#9.
  • Loading branch information
brettdh committed Sep 11, 2020
1 parent 445d8d1 commit 4005ed7
Show file tree
Hide file tree
Showing 30 changed files with 759 additions and 5 deletions.
51 changes: 51 additions & 0 deletions README.md
Expand Up @@ -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/<YourAppName>/Info.plist',
updater: require.resolve('standard-version-expo/ios/native/app-version'),
},
{
filename: 'ios/<YourAppName>/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.

Expand All @@ -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/<YourAppName>/Info.plist` | Replace `CFBundleShortVersionString` with the exact calculated semver.
`native/ios/buildnum/code` | `36030201` | `ios/<YourAppName>/Info.plist` | Replace `CFBundleVersion` with the [method described by Maxi Rosson][link-version-code].
`native/ios/buildnum/increment` | `8` | `ios/<YourAppName>/Info.plist` | Replace `CFBundleVersion` with an incremental version.
`native/ios/buildnum/version` | `3.2.1` | `ios/<YourAppName>/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.
Expand All @@ -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/
1 change: 1 addition & 0 deletions android/native/app-version.js
@@ -0,0 +1 @@
module.exports = require('../../build/bumpers/native/android-app-version');
1 change: 1 addition & 0 deletions android/native/buildnum/code.js
@@ -0,0 +1 @@
module.exports = require('../../build/bumpers/native/buildnum/android-code');
1 change: 1 addition & 0 deletions android/native/buildnum/increment.js
@@ -0,0 +1 @@
module.exports = require('../../build/bumpers/native/buildnum/android-increment');
1 change: 1 addition & 0 deletions android/native/buildnum/index.js
@@ -0,0 +1 @@
module.exports = require('../../build/bumpers/native/buildnum/android-code');
1 change: 1 addition & 0 deletions ios/native/app-version.js
@@ -0,0 +1 @@
module.exports = require('../../build/bumpers/native/ios-app-version');
1 change: 1 addition & 0 deletions ios/native/buildnum/code.js
@@ -0,0 +1 @@
module.exports = require('../../build/bumpers/native/buildnum/ios-code');
1 change: 1 addition & 0 deletions ios/native/buildnum/increment.js
@@ -0,0 +1 @@
module.exports = require('../../build/bumpers/native/buildnum/ios-increment');
1 change: 1 addition & 0 deletions ios/native/buildnum/index.js
@@ -0,0 +1 @@
module.exports = require('../../build/bumpers/native/buildnum/ios-version');
1 change: 1 addition & 0 deletions ios/native/buildnum/version.js
@@ -0,0 +1 @@
module.exports = require('../../build/bumpers/native/buildnum/ios-version');
12 changes: 10 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -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": {
Expand Down
11 changes: 11 additions & 0 deletions 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;
42 changes: 42 additions & 0 deletions 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),
),
),
});
24 changes: 24 additions & 0 deletions 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));
};
42 changes: 42 additions & 0 deletions 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),
),
),
});
24 changes: 24 additions & 0 deletions 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));
};
11 changes: 11 additions & 0 deletions 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;
117 changes: 117 additions & 0 deletions 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)
);

0 comments on commit 4005ed7

Please sign in to comment.