Skip to content

Commit 1eb1e75

Browse files
Merge pull request #12 from fingerprintjs/INTER-1221/podspec-dsl-format
Add podspec DSL support and deprecate outdated parameter
2 parents ee9d833 + 82aad2a commit 1eb1e75

File tree

11 files changed

+238
-39
lines changed

11 files changed

+238
-39
lines changed

.github/workflows/coverage-diff.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ jobs:
99
pull-requests: write
1010
contents: read
1111
uses: fingerprintjs/dx-team-toolkit/.github/workflows/coverage-diff.yml@v1
12+
with:
13+
runsOn: 'macos-latest'

README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
66
## Overview
77

8-
This plugin retrieves native dependency version information from iOS and/or Android projects and integrates it into the semantic release workflow generate notes steps.
8+
This plugin retrieves native dependency version information from iOS and/or Android projects and integrates it into the
9+
semantic release workflow generate notes steps.
910

10-
- Extracts dependency versions from podspec json file (iOS) and/or Gradle task output (Android)
11+
- Extracts dependency versions from podspec file (iOS) and/or Gradle task output (Android)
1112
- Ensures version consistency in release notes
1213
- Automates version retrieval for better release documentation
1314

@@ -31,7 +32,7 @@ Add the plugin to your `.releaserc` configuration:
3132
"heading": "Supported Native SDK Version Range",
3233
"platforms": {
3334
"iOS": {
34-
"podSpecJsonPath": "RNFingerprintjsPro.podspec.json",
35+
"podSpecPath": "RNFingerprintjsPro.podspec.json",
3536
"dependencyName": "FingerprintPro",
3637
"displayName": "Fingerprint iOS SDK"
3738
},
@@ -54,7 +55,7 @@ Add the plugin to your `.releaserc` configuration:
5455
| `heading` | `string` | `Native Dependencies` | Optional h3 heading shown before listing platform specific version ranges. |
5556
| `platforms` | `object` | | Top-level object defining configuration for each platform. |
5657
| `platforms.iOS` | `object` | | Configuration for the iOS dependency version resolution. |
57-
| `platforms.iOS.podSpecJsonPath` | `string` | | Path to the PODSPEC json file containing iOS dependency metadata. |
58+
| `platforms.iOS.podSpecPath` | `string` | | Path to the PODSPEC file containing iOS dependency metadata. |
5859
| `platforms.iOS.dependencyName` | `string` | | Name of the dependency to extract the version. |
5960
| `platforms.iOS.displayName` | `string` | `iOS` | Name for the iOS dependency shown in release notes. |
6061
| `platforms.android` | `object` | | Configuration for the Android dependency version resolution. |
@@ -67,7 +68,7 @@ Add the plugin to your `.releaserc` configuration:
6768
6869
## How It Works
6970

70-
- The plugin reads version information from podspec json file (iOS) and/or a custom Gradle task output (Android).
71+
- The plugin reads version information from podspec file (iOS) and/or a custom Gradle task output (Android).
7172
- It automatically includes the extracted versions in the release notes.
7273
- Helps maintain transparency about dependency versions used in each release.
7374

@@ -104,7 +105,6 @@ We welcome contributions! To get started with development:
104105
- **[Jest][jest]** for testing
105106
- **[Commitizen][commitizen]** for conventional commits
106107

107-
108108
### Setup
109109

110110
1. Clone the repository
@@ -127,7 +127,7 @@ We welcome contributions! To get started with development:
127127
```shell
128128
pnpm start
129129
```
130-
or build the project manually:
130+
or build the project manually:
131131
```shell
132132
pnpm build
133133
```
@@ -155,6 +155,9 @@ We welcome contributions! To get started with development:
155155
MIT
156156

157157
[husky]: https://typicode.github.io/husky
158+
158159
[lint-staged]: https://github.com/lint-staged/lint-staged
160+
159161
[jest]: https://jestjs.io
162+
160163
[commitizen]: https://commitizen-tools.github.io/commitizen

src/errors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class JsonParseError extends Error {
2+
constructor(podspecFilePath: string) {
3+
super(`${podspecFilePath} file cannot be parsed as JSON.`)
4+
}
5+
}

src/gradle.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { join } from 'node:path'
22
import { access, constants } from 'node:fs'
33
import { exec } from 'node:child_process'
4+
import { isNotFoundErrorCode } from './utils'
45

56
/**
67
* Check if gradle is installed to system-wide.
@@ -12,8 +13,7 @@ export function isGradleAvailable(): Promise<boolean> {
1213
return new Promise((resolve, reject) => {
1314
exec('gradle --version', (err) => {
1415
if (err) {
15-
// 127 means command not found (https://tldp.org/LDP/abs/html/exitcodes.html)
16-
if (err.code === 127) {
16+
if (isNotFoundErrorCode(err.code)) {
1717
resolve(false)
1818
}
1919
reject(err)

src/platforms/iOS.ts

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,94 @@
11
import { join } from 'node:path'
22
import { readFileSync } from 'node:fs'
33
import type { GenerateNotesContext } from 'semantic-release'
4+
import { isPodAvailable, podIpcSpec, PodspecJson } from '../pods'
5+
import { JsonParseError } from '../errors'
46

57
export interface IOSPlatformConfiguration {
6-
podSpecJsonPath: string | undefined
8+
/**
9+
* @deprecated use `podspecPath` instead
10+
* */
11+
podSpecJsonPath?: string | undefined
12+
podspecPath: string | undefined
713
dependencyName: string | undefined
814
displayName: string | undefined
915
}
1016

11-
type PodspecJson = {
12-
dependencies: {
13-
[key: string]: [string]
14-
}
15-
}
16-
17-
export type IOSResolveContext = Pick<GenerateNotesContext, 'cwd'>
17+
export type IOSResolveContext = Pick<GenerateNotesContext, 'cwd' | 'logger'>
1818

1919
export const resolve = async (
20-
{ cwd }: IOSResolveContext,
21-
{ podSpecJsonPath, dependencyName }: IOSPlatformConfiguration
20+
{ cwd, logger }: IOSResolveContext,
21+
{ podSpecJsonPath, podspecPath, dependencyName }: IOSPlatformConfiguration
2222
) => {
2323
if (!cwd) {
2424
throw new Error(`Current working directory is required to detect iOS dependency version range.`)
2525
}
2626

27-
if (!podSpecJsonPath) {
28-
throw new Error('iOS Podspec Json path should be defined.')
29-
}
30-
3127
if (!dependencyName) {
3228
throw new Error('iOS Dependency name should be defined.')
3329
}
3430

35-
const jsonFile = join(cwd, podSpecJsonPath)
31+
let podspecPathParam: string | undefined
32+
if (podspecPath) {
33+
podspecPathParam = podspecPath
34+
} else if (podSpecJsonPath) {
35+
logger.warn('[DEPRECATED] Use `platforms.iOS.podspecPath` instead of `platform.iOS.podSpecJsonPath`.')
36+
podspecPathParam = podSpecJsonPath
37+
}
38+
if (!podspecPathParam) {
39+
throw new Error('iOS Podspec path should be defined.')
40+
}
41+
42+
let podspecContents: PodspecJson | undefined
43+
44+
try {
45+
podspecContents = readPodspecJson(cwd, podspecPathParam)
46+
} catch (e) {
47+
if (!(e instanceof JsonParseError)) {
48+
throw e
49+
}
50+
51+
podspecContents = await readPodspecDSL(podspecPathParam)
52+
}
53+
54+
if (!podspecContents.dependencies || !podspecContents.dependencies[dependencyName]) {
55+
throw new Error(`${podspecPathParam} file does not contain '${dependencyName}' in dependencies.`)
56+
}
57+
58+
return podspecContents.dependencies[dependencyName].join(' and ')
59+
}
60+
61+
function readPodspecDSL(podspecFilePath: string): Promise<PodspecJson> {
62+
if (!isPodAvailable()) {
63+
throw new Error(`Pods not found in your system.`)
64+
}
65+
66+
return podIpcSpec(podspecFilePath)
67+
}
68+
69+
function readPodspecJson(cwd: string, podspecFilePath: string): PodspecJson {
70+
const resolvedPodspecPath = join(cwd, podspecFilePath)
3671

3772
let fileContent: string
3873
try {
39-
fileContent = readFileSync(jsonFile, 'utf8')
74+
fileContent = readFileSync(resolvedPodspecPath, 'utf8')
4075
} catch (error: any) {
4176
switch (error.code) {
4277
case 'ENOENT':
43-
throw new Error(`${podSpecJsonPath} file does not exist.`)
78+
throw new Error(`${podspecFilePath} file does not exist.`)
4479
case 'EACCES':
45-
throw new Error(`${podSpecJsonPath} file cannot be accessed.`)
80+
throw new Error(`${podspecFilePath} file cannot be accessed.`)
4681
default:
47-
throw new Error(`${podSpecJsonPath} file cannot be read. Error: ${error.message}`)
82+
throw new Error(`${podspecFilePath} file cannot be read. Error: ${error.message}`)
4883
}
4984
}
5085

5186
let data: PodspecJson
5287
try {
5388
data = JSON.parse(fileContent) as PodspecJson
5489
} catch (error) {
55-
throw new Error(`${podSpecJsonPath} file cannot be parsed as JSON.`)
56-
}
57-
58-
if (!data.dependencies || !data.dependencies[dependencyName]) {
59-
throw new Error(`${podSpecJsonPath} file does not contain '${dependencyName}' in dependencies.`)
90+
throw new JsonParseError(podspecFilePath)
6091
}
6192

62-
return data.dependencies[dependencyName].join(' and ')
93+
return data
6394
}

src/pods.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { exec } from 'child_process'
2+
import { isNotFoundErrorCode } from './utils'
3+
import { JsonParseError } from './errors'
4+
5+
/**
6+
* Checks whether CocoaPods are installed and available on the system by executing the `pod --version` command.
7+
* Resolves to `true` if CocoaPods are available, resolves to `false` if CocoaPods are not installed,
8+
* and rejects with an error if another issue occurs.
9+
*
10+
* @return {Promise<boolean>} A promise that resolves to `true` if CocoaPods are installed `false` otherwise.
11+
*/
12+
export function isPodAvailable(): Promise<boolean> {
13+
return new Promise((resolve, reject) => {
14+
exec('pod --version', (err) => {
15+
if (err) {
16+
if (isNotFoundErrorCode(err.code)) {
17+
resolve(false)
18+
}
19+
reject(err)
20+
}
21+
22+
resolve(true)
23+
})
24+
})
25+
}
26+
27+
/**
28+
* Executes the `pod ipc spec` command on the given podspec file and returns the parsed result as an object.
29+
*
30+
* @param {string} podspecFile - The path to the `.podspec` file to be processed.
31+
* @return {Promise<PodspecJson>} A promise that resolves to the parsed PodspecJson object or rejects with an error if the command fails.
32+
*/
33+
export function podIpcSpec(podspecFile: string): Promise<PodspecJson> {
34+
return new Promise<PodspecJson>((resolve, reject) => {
35+
exec(`pod ipc spec ${podspecFile}`, (err, stdout) => {
36+
if (err) {
37+
reject(err)
38+
}
39+
40+
try {
41+
resolve(JSON.parse(stdout) as PodspecJson)
42+
} catch {
43+
throw new JsonParseError(podspecFile)
44+
}
45+
})
46+
})
47+
}
48+
49+
export type PodspecJson = {
50+
dependencies: {
51+
[key: string]: [string]
52+
}
53+
}

src/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ export function humanizeMavenStyleVersionRange(versionRange: string) {
88
.replace(/\s*\[\s*(.*?)\s*]/g, '>=$1')
99
.replace(/\s*\(\s*(.*?)\s*\)/g, '>$1')
1010
}
11+
12+
export function isNotFoundErrorCode(code?: number) {
13+
// 127 means command not found (https://tldp.org/LDP/abs/html/exitcodes.html)
14+
return code === 127
15+
}

test/iOS.test.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
import { describe, expect, it } from '@jest/globals'
22
import { cwd } from 'node:process'
33
import { IOSResolveContext, resolve } from '../src/platforms/iOS'
4+
import { Signale } from 'signale'
45

56
describe('Test for iOS platform', () => {
7+
const logger = new Signale({ disabled: true })
68
const dependencyName = 'FingerprintPro'
79
const ctx = {
810
cwd: cwd(),
11+
logger,
912
} satisfies IOSResolveContext
1013

11-
it('returns correct version', async () => {
14+
it('returns correct version with json', async () => {
15+
const iosVersion = await resolve(ctx, {
16+
podspecPath: 'test/project/ios/podspec.json',
17+
dependencyName,
18+
displayName: undefined,
19+
})
20+
expect(iosVersion).toBe('>= 1.2.3 and < 4.5.6')
21+
})
22+
23+
it('returns correct version with json using deprecated podSpecJsonPath', async () => {
24+
//@ts-expect-error
1225
const iosVersion = await resolve(ctx, {
1326
podSpecJsonPath: 'test/project/ios/podspec.json',
1427
dependencyName,
@@ -17,10 +30,19 @@ describe('Test for iOS platform', () => {
1730
expect(iosVersion).toBe('>= 1.2.3 and < 4.5.6')
1831
})
1932

33+
it('returns correct version with dsl', async () => {
34+
const iosVersion = await resolve(ctx, {
35+
podspecPath: 'test/project/ios/Test.podspec',
36+
dependencyName,
37+
displayName: undefined,
38+
})
39+
expect(iosVersion).toBe('>= 1.2.3 and < 4.5.6')
40+
})
41+
2042
it('throws error when file not found', async () => {
2143
await expect(
2244
resolve(ctx, {
23-
podSpecJsonPath: 'nonExists',
45+
podspecPath: 'nonExists',
2446
dependencyName,
2547
displayName: undefined,
2648
})
@@ -34,7 +56,7 @@ describe('Test for iOS platform', () => {
3456
})
3557

3658
await expect(
37-
resolve(ctx, { podSpecJsonPath: 'not-readable.podspec.json', dependencyName, displayName: undefined })
59+
resolve(ctx, { podspecPath: 'not-readable.podspec.json', dependencyName, displayName: undefined })
3860
).rejects.toThrowErrorMatchingSnapshot('podspecNotReadable')
3961

4062
readFileSyncMock.mockRestore()
@@ -43,7 +65,7 @@ describe('Test for iOS platform', () => {
4365
it('throws error when path is a directory', async () => {
4466
await expect(
4567
resolve(ctx, {
46-
podSpecJsonPath: 'test',
68+
podspecPath: 'test',
4769
dependencyName,
4870
displayName: undefined,
4971
})

test/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const cwd = process.cwd()
99
const pluginConfig = {
1010
platforms: {
1111
iOS: {
12-
podSpecJsonPath: 'test/project/ios/podspec.json',
12+
podspecPath: 'test/project/ios/podspec.json',
1313
dependencyName: 'FingerprintPro',
1414
displayName: 'Fingerprint iOS SDK',
1515
},

0 commit comments

Comments
 (0)