Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ no native file is left.
| `--write` | Update the native files instead of only comparing them |
| `--version-name <name>` | Use this version name instead of the `package.json` version. Requires `--version-code` unless it is semver. |
| `--version-code <code>` | Use this version code instead of the calculated one |
| `--reserve-builds <n>` | Multiply the calculated version code by `n` so each version owns `n` consecutive codes, e.g. for CI builds that bump the code per build (`100` turns `10203` into `1020300`). Ignored with `--version-code`. |
| `--reserve-builds <n>` | Multiply the calculated version code by `n` so each version owns `n` consecutive codes, e.g. for CI builds that bump the code per build (`100` turns `10203` into `1020300`). Ignored with `--version-code`. Best set once as `reserveBuilds` in `package.json`, see [Configuration](#configuration). |
| `--skip-android`, `--skip-ios` | Ignore that platform |
| `--project-dir <dir>` | Project root (default: current directory) |
| `--gradle-path <path>` | Android `build.gradle` to use, instead of `android/app/build.gradle` |
Expand All @@ -105,6 +105,32 @@ Relative `--gradle-path` and `--pbxproj-path` values are resolved against the
project directory. Version codes must be positive integers up to
`2147483647`, the 32-bit limit both platforms enforce.

### Configuration

Settings that are permanent for a project go under the `rn-version-sync` key
of `package.json`, so the check, `--write` and the `version` script all use
the same values without repeating them on every call:

```json
{
"rn-version-sync": {
"reserveBuilds": 100
}
}
```

| Setting | Option |
| ------- | ------ |
| `reserveBuilds` (integer) | `--reserve-builds` |
| `gradlePath` (string) | `--gradle-path` |
| `pbxprojPath` (string) | `--pbxproj-path` |
| `configuration` (string) | `--configuration` |
| `skipAndroid`, `skipIos` (boolean) | `--skip-android`, `--skip-ios` |

A command line option takes precedence over the configured value. Relative
paths are resolved against the project directory, and an unknown setting or a
value of the wrong type is an error.

## Reading values from native files

The `--print*` flags read what is actually written in the native files instead
Expand Down Expand Up @@ -205,7 +231,7 @@ steps:
The same functionality is available as a module, with type definitions:

```js
const { checkVersions, syncVersions, readNativeValues } = require('rn-version-sync');
const { checkVersions, syncVersions, readNativeValues, loadConfig } = require('rn-version-sync');

checkVersions(process.cwd());
// {
Expand All @@ -225,10 +251,15 @@ syncVersions(process.cwd(), { reserveBuilds: 100 });

readNativeValues(process.cwd(), 'ios', { configuration: 'Staging' });
// { appId: 'com.example.app.staging', versionName: '1.2.3', versionCode: '1020300' }

loadConfig(process.cwd());
// { reserveBuilds: 100 }
```

`resolveVersions`, `formatTemplate` and `formatEnv` back the target row,
`--format` and `--print-env` in the same way.
`--format` and `--print-env` in the same way. Like the CLI, all functions fill
in options that are not passed explicitly from the `rn-version-sync`
configuration in `package.json`.

## Requirements

Expand Down
76 changes: 76 additions & 0 deletions src/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,79 @@ describe('cli', () => {
);
});

it('applies the configuration in package.json', () => {
project = new TestProject({
version: '1.2.3',
config: { reserveBuilds: 100, skipIos: true },
android: { versionName: '1.2.3', versionCode: 1020300 },
ios: false,
});

expect(run(project.root)).toEqual({
status: 0,
stdout: [
'PLATFORM APP ID VERSION STATUS',
JS_ROW,
'android com.testapp 1.2.3 (1020300) ok',
'',
].join('\n'),
stderr: '',
});
});

it('prefers command line options over the configuration', () => {
project = new TestProject({
version: '1.2.3',
config: { reserveBuilds: 100, skipIos: true },
ios: false,
});

const result = run(project.root, '--write', '--reserve-builds', '10');
expect(result.stdout).toContain(
'android com.testapp 1.2.3 (102030) updated\n',
);
});

it('labels the iOS row with the configuration from package.json', () => {
project = new TestProject({
version: '1.2.3',
config: { configuration: 'Debug' },
android: false,
ios: [
{
name: 'Debug',
version: '1.2.3',
buildNumber: '10203',
bundleId: 'com.testapp',
},
{
name: 'Release',
version: '1.0.0',
buildNumber: '1',
bundleId: 'com.testapp',
},
],
});

expect(run(project.root).stdout).toContain(
'ios (Debug) com.testapp 1.2.3 (10203) ok\n',
);
});

it('rejects an invalid configuration', () => {
project = new TestProject({
version: '1.2.3',
config: { reserveBuild: 100 },
});

expect(run(project.root)).toEqual({
status: 1,
stdout: '',
stderr:
'Error: Invalid "rn-version-sync" configuration in package.json: unknown setting "reserveBuild" (available: reserveBuilds, gradlePath, pbxprojPath, configuration, skipAndroid, skipIos)\n',
});
});

it('uses --project-dir instead of the working directory', () => {
project = new TestProject({ version: '1.2.3' });
const other = new TestProject({ version: '9.9.9' });
Expand Down Expand Up @@ -462,6 +535,9 @@ describe('cli', () => {
expect(result.stdout).toContain('Usage: rn-version-sync [options]');
expect(result.stdout).toContain('--write');
expect(result.stdout).toContain('--reserve-builds <n>');
expect(result.stdout).toContain(
'"rn-version-sync": { "reserveBuilds": 100 }',
);
});
});
});
95 changes: 95 additions & 0 deletions src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { loadConfig } from '../config';
import { TestProject } from './helpers';

const INVALID = 'Invalid "rn-version-sync" configuration in package.json: ';

describe('loadConfig', () => {
let project: TestProject;

afterEach(() => {
project?.cleanup();
});

it('returns an empty configuration without the package.json key', () => {
project = new TestProject({ android: false, ios: false });

expect(loadConfig(project.root)).toEqual({});
});

it('returns an empty configuration when package.json is missing', () => {
project = new TestProject({ android: false, ios: false });
fs.rmSync(path.join(project.root, 'package.json'));

expect(loadConfig(project.root)).toEqual({});
});

it('reads the settings and resolves paths against the project directory', () => {
project = new TestProject({
android: false,
ios: false,
config: {
reserveBuilds: 100,
gradlePath: 'app/build.gradle',
pbxprojPath: '/elsewhere/App.xcodeproj/project.pbxproj',
configuration: 'Staging',
skipAndroid: false,
skipIos: true,
},
});

expect(loadConfig(project.root)).toEqual({
reserveBuilds: 100,
gradlePath: path.join(project.root, 'app', 'build.gradle'),
pbxprojPath: '/elsewhere/App.xcodeproj/project.pbxproj',
configuration: 'Staging',
skipAndroid: false,
skipIos: true,
});
});

it('rejects a configuration that is not an object', () => {
for (const config of [null, 100, 'reserveBuilds', [100]]) {
project = new TestProject({ android: false, ios: false, config });

expect(() => loadConfig(project.root)).toThrow(
`${INVALID}must be an object`,
);
project.cleanup();
}
});

it('rejects an unknown setting', () => {
project = new TestProject({
android: false,
ios: false,
config: { reserveBuild: 100 },
});

expect(() => loadConfig(project.root)).toThrow(
`${INVALID}unknown setting "reserveBuild" (available: reserveBuilds, gradlePath, pbxprojPath, configuration, skipAndroid, skipIos)`,
);
});

it('rejects values of the wrong type', () => {
const cases: [unknown, string][] = [
[{ reserveBuilds: '100' }, 'reserveBuilds must be a positive integer'],
[{ reserveBuilds: 0 }, 'reserveBuilds must be a positive integer'],
[{ reserveBuilds: 1.5 }, 'reserveBuilds must be a positive integer'],
[{ gradlePath: 5 }, 'gradlePath must be a string'],
[{ pbxprojPath: null }, 'pbxprojPath must be a string'],
[{ configuration: true }, 'configuration must be a string'],
[{ skipAndroid: 'yes' }, 'skipAndroid must be a boolean'],
[{ skipIos: 1 }, 'skipIos must be a boolean'],
];

for (const [config, message] of cases) {
project = new TestProject({ android: false, ios: false, config });

expect(() => loadConfig(project.root)).toThrow(`${INVALID}${message}`);
project.cleanup();
}
});
});
10 changes: 9 additions & 1 deletion src/__tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export interface ProjectOptions {
iosTargets?: PbxprojTarget[];
/** Project-level build configurations; default to the targets' names without version settings */
iosProjectConfigs?: PbxprojBuildConfig[];
/** Written as the "rn-version-sync" key of package.json, even when invalid */
config?: unknown;
}

export const APPLICATION_PRODUCT_TYPE = 'com.apple.product-type.application';
Expand Down Expand Up @@ -70,7 +72,13 @@ export class TestProject {
fs.writeFileSync(
path.join(this.root, 'package.json'),
JSON.stringify(
{ name: 'test-app', version: options.version ?? defaults.version },
{
name: 'test-app',
version: options.version ?? defaults.version,
...(options.config !== undefined && {
'rn-version-sync': options.config,
}),
},
null,
2,
),
Expand Down
Loading