Skip to content

Commit 2f41e0b

Browse files
authored
Merge pull request #3 from fingerprintjs/feat/platform-specific-run-inter-1194
Add platform-specific run support with legacy compatibility Related-Task: INTER-1194
2 parents b2321e6 + cc1f704 commit 2f41e0b

13 files changed

+382
-247
lines changed

README.md

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

8-
This plugin retrieves native dependency version information from iOS and 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 semantic release workflow generate notes steps.
99

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

@@ -28,25 +28,27 @@ Add the plugin to your `.releaserc` configuration:
2828
[
2929
"@fingerprintjs/semantic-release-native-dependency-plugin",
3030
{
31-
"iOS": {
32-
"podSpecJsonPath": "RNFingerprintjsPro.podspec.json",
33-
"dependencyName": "FingerprintPro",
34-
"displayName": "Fingerprint iOS SDK"
35-
},
36-
"android": {
37-
"path": "android",
38-
"gradleTaskName": "printFingerprintNativeSDKVersion",
39-
"displayName": "Fingerprint Android SDK"
31+
"platforms": {
32+
"iOS": {
33+
"podSpecJsonPath": "RNFingerprintjsPro.podspec.json",
34+
"dependencyName": "FingerprintPro",
35+
"displayName": "Fingerprint iOS SDK"
36+
},
37+
"android": {
38+
"path": "android",
39+
"gradleTaskName": "printFingerprintNativeSDKVersion",
40+
"displayName": "Fingerprint Android SDK"
41+
}
4042
}
4143
}
42-
],
44+
]
4345
]
4446
}
4547
```
4648

4749
## How It Works
4850

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

src/@types/pluginConfig.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import { AndroidPlatformConfiguration } from '../platforms/android'
2+
import { IOSPlatformConfiguration } from '../platforms/iOS'
3+
4+
export interface PlatformConfig {
5+
iOS?: IOSPlatformConfiguration
6+
android?: AndroidPlatformConfiguration
7+
}
8+
19
export default interface PluginConfig {
2-
iOS: {
3-
podSpecJsonPath: string
4-
dependencyName: string | undefined
5-
displayName: string | undefined
6-
}
7-
android: {
8-
path: string
9-
gradleTaskName: string | undefined
10-
displayName: string | undefined
11-
}
10+
platforms: PlatformConfig
11+
/** @deprecated Use `platforms.iOS` instead */
12+
iOS?: IOSPlatformConfiguration
13+
/** @deprecated Use `platforms.android` instead */
14+
android?: AndroidPlatformConfiguration
1215
}

src/generateNotes.ts

Lines changed: 34 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,148 +1,49 @@
1-
import { join } from 'node:path'
21
import type { GenerateNotesContext } from 'semantic-release'
3-
import { spawn } from 'node:child_process'
4-
import type { Signale } from 'signale'
5-
import PluginConfig from './@types/pluginConfig'
6-
import { getCommand } from './gradle'
7-
import { humanizeMavenStyleVersionRange } from './utils'
8-
import { readFileSync } from 'node:fs'
9-
10-
export async function getAndroidVersion(
11-
cwd: string,
12-
androidPath: string,
13-
androidGradleTaskName: string,
14-
env: NodeJS.ProcessEnv,
15-
logger: Signale
16-
): Promise<string> {
17-
const androidFullPath = join(cwd, androidPath)
18-
const command = await getCommand(androidFullPath)
19-
20-
return new Promise((resolve, reject) => {
21-
const child = spawn(command, [androidGradleTaskName, '-q', '--console=plain'], {
22-
cwd: androidPath,
23-
env,
24-
detached: true,
25-
stdio: ['inherit', 'pipe', 'pipe'],
26-
})
27-
if (child.stdout === null) {
28-
reject(new Error('Unexpected error: stdout of subprocess is null'))
29-
return
2+
import PluginConfig, { PlatformConfig } from './@types/pluginConfig'
3+
import { resolve as androidResolve } from './platforms/android'
4+
import { resolve as iOSResolve } from './platforms/iOS'
5+
6+
const generateNotes = async (config: PluginConfig, ctx: GenerateNotesContext) => {
7+
const platformVersions: { [key in keyof PlatformConfig]?: string } = {}
8+
9+
// Normalize plugin config: prefer `platforms`, fall back to legacy top-level platform keys for backward compatibility
10+
// TODO: Remove support for top-level `android` and `iOS` keys in the next major release (BC)
11+
const platforms: PlatformConfig = config.platforms || {}
12+
if (!config.platforms) {
13+
if (config.android) {
14+
ctx.logger.warn('[DEPRECATED] Use `platforms.android` instead of top-level `android` in plugin config.')
15+
platforms.android = config.android
3016
}
31-
if (child.stderr === null) {
32-
reject(new Error('Unexpected error: stderr of subprocess is null'))
33-
return
34-
}
35-
36-
let androidVersion: string | null = null
37-
child.stdout.on('data', (line: Buffer) => {
38-
if (!line || line.toString().trim() === '') {
39-
return
40-
}
41-
42-
logger.debug(`Gradle stdout: ${line}`)
43-
androidVersion = line.toString().trim()
44-
})
45-
child.stderr.on('data', (line: Buffer) => {
46-
logger.error(line.toString().trim())
47-
})
48-
child.on('close', (code: number) => {
49-
if (code !== 0) {
50-
reject(new Error(`Unexpected error: Gradle failed with status code ${code}`))
51-
return
52-
}
53-
54-
if (androidVersion === null) {
55-
reject(new Error(`Could not read output of \`${androidGradleTaskName}\` gradle task.`))
56-
return
57-
}
58-
59-
resolve(androidVersion)
60-
})
61-
child.on('error', (err) => {
62-
logger.error(err)
63-
reject(err)
64-
})
65-
})
66-
}
67-
68-
type PodspecJson = {
69-
dependencies: {
70-
[key: string]: [string]
71-
}
72-
}
73-
74-
export const getIOSVersion = async (cwd: string, iOSPodSpecJsonPath: string, dependencyName: string) => {
75-
const jsonFile = join(cwd, iOSPodSpecJsonPath)
7617

77-
let fileContent: string
78-
try {
79-
fileContent = readFileSync(jsonFile, 'utf8')
80-
} catch (error: any) {
81-
switch (error.code) {
82-
case 'ENOENT':
83-
throw new Error(`${iOSPodSpecJsonPath} file does not exist.`)
84-
case 'EACCES':
85-
throw new Error(`${iOSPodSpecJsonPath} file cannot be accessed.`)
86-
default:
87-
throw new Error(`${iOSPodSpecJsonPath} file cannot be read.`)
18+
if (config.iOS) {
19+
ctx.logger.warn('[DEPRECATED] Use `platforms.iOS` instead of top-level `iOS` in plugin config.')
20+
platforms.iOS = config.iOS
8821
}
8922
}
9023

91-
let data: PodspecJson
92-
try {
93-
data = JSON.parse(fileContent) as PodspecJson
94-
} catch (error) {
95-
throw new Error(`${iOSPodSpecJsonPath} file cannot be parsed as JSON.`)
24+
if (!platforms.android && !platforms.iOS) {
25+
throw new Error('No platforms specified. You must configure at least one platform under `platforms`.')
9626
}
9727

98-
if (!data.dependencies || !data.dependencies[dependencyName]) {
99-
throw new Error(`${iOSPodSpecJsonPath} file does not contain '${dependencyName}' in dependencies.`)
28+
if (platforms.android) {
29+
const androidVersion = await androidResolve(ctx, platforms.android)
30+
platformVersions.android = androidVersion
31+
ctx.logger.log(`Detected Android Version: \`${androidVersion}\``)
10032
}
10133

102-
return data.dependencies[dependencyName].join(' and ')
103-
}
104-
105-
const generateNotes = async ({ iOS, android }: PluginConfig, { logger, cwd, env }: GenerateNotesContext) => {
106-
if (!cwd) {
107-
throw new Error(`Current working directory is required to detect native dependency versions.`)
108-
}
109-
110-
if (!android.gradleTaskName) {
111-
throw new Error('Android gradle task name should be defined.')
112-
}
113-
114-
if (!iOS.podSpecJsonPath) {
115-
throw new Error('iOS Podspec Json path should be defined.')
116-
}
117-
118-
if (!iOS.dependencyName) {
119-
throw new Error('iOS Dependency name should be defined.')
34+
if (platforms.iOS) {
35+
const iOSVersion = await iOSResolve(ctx, platforms.iOS)
36+
platformVersions.iOS = iOSVersion
37+
ctx.logger.log(`Detected iOS Version: \`${iOSVersion}\``)
12038
}
12139

122-
if (!android.displayName) {
123-
android.displayName = 'Android Dependency'
124-
}
125-
126-
if (!iOS.displayName) {
127-
iOS.displayName = 'iOS Dependency'
128-
}
129-
130-
const androidVersion = await getAndroidVersion(
131-
cwd,
132-
android.path,
133-
android.gradleTaskName,
134-
env as NodeJS.ProcessEnv,
135-
logger
136-
)
137-
const humanizedAndroidVersion = humanizeMavenStyleVersionRange(androidVersion)
138-
logger.log(`Detected ${android.displayName} Version: \`${androidVersion}\` \`${humanizedAndroidVersion}\``)
139-
140-
const iosVersion = await getIOSVersion(cwd, iOS.podSpecJsonPath, iOS.dependencyName)
141-
logger.log(`Detected ${iOS.displayName} Version: \`${iosVersion}\``)
142-
143-
return `${android.displayName} Version Range: **\`${humanizedAndroidVersion}\`**
144-
145-
${iOS.displayName} Version Range: **\`${iosVersion}\`**`
40+
return Object.keys(platformVersions)
41+
.map((platformKey) => {
42+
const platform = platformKey as keyof PlatformConfig
43+
const version = platformVersions[platform]
44+
return `${platforms[platform]?.displayName ?? platform} Version Range: **\`${version}\`**`
45+
})
46+
.join('\n\n')
14647
}
14748

14849
export default generateNotes

src/platforms/android.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { humanizeMavenStyleVersionRange } from '../utils'
2+
import type { GenerateNotesContext } from 'semantic-release'
3+
import type { Signale } from 'signale'
4+
import { join } from 'node:path'
5+
import { getCommand } from '../gradle'
6+
import { spawn } from 'node:child_process'
7+
8+
export interface AndroidPlatformConfiguration {
9+
path: string | undefined
10+
gradleTaskName: string | undefined
11+
displayName: string | undefined
12+
}
13+
14+
async function runGradleTask(
15+
cwd: string,
16+
androidPath: string,
17+
androidGradleTaskName: string,
18+
env: NodeJS.ProcessEnv,
19+
logger: Signale
20+
): Promise<string> {
21+
const androidFullPath = join(cwd, androidPath)
22+
const command = await getCommand(androidFullPath)
23+
24+
return new Promise((resolve, reject) => {
25+
const child = spawn(command, [androidGradleTaskName, '-q', '--console=plain'], {
26+
cwd: androidPath,
27+
env,
28+
detached: true,
29+
stdio: ['inherit', 'pipe', 'pipe'],
30+
})
31+
if (child.stdout === null) {
32+
reject(new Error('Unexpected error: stdout of subprocess is null'))
33+
return
34+
}
35+
if (child.stderr === null) {
36+
reject(new Error('Unexpected error: stderr of subprocess is null'))
37+
return
38+
}
39+
40+
let androidVersion: string | null = null
41+
child.stdout.on('data', (line: Buffer) => {
42+
if (!line || line.toString().trim() === '') {
43+
return
44+
}
45+
46+
logger.debug(`Gradle stdout: ${line}`)
47+
androidVersion = line.toString().trim()
48+
})
49+
child.stderr.on('data', (line: Buffer) => {
50+
logger.error(line.toString().trim())
51+
})
52+
child.on('close', (code: number) => {
53+
if (code !== 0) {
54+
reject(new Error(`Unexpected error: Gradle failed with status code ${code}`))
55+
return
56+
}
57+
58+
if (androidVersion === null) {
59+
reject(new Error(`Could not read output of \`${androidGradleTaskName}\` gradle task.`))
60+
return
61+
}
62+
63+
resolve(androidVersion)
64+
})
65+
child.on('error', (err) => {
66+
logger.error(err)
67+
reject(err)
68+
})
69+
})
70+
}
71+
72+
export type AndroidResolveContext = Pick<GenerateNotesContext, 'logger' | 'cwd' | 'env'>
73+
74+
export const resolve = async (
75+
{ logger, cwd, env }: AndroidResolveContext,
76+
{ path, gradleTaskName }: AndroidPlatformConfiguration
77+
) => {
78+
if (!cwd) {
79+
throw new Error(`Current working directory is required to detect android dependency version range.`)
80+
}
81+
82+
if (!path) {
83+
throw new Error('Android path should be defined.')
84+
}
85+
86+
if (!gradleTaskName) {
87+
throw new Error('Android gradle task name should be defined.')
88+
}
89+
90+
const androidVersion = await runGradleTask(cwd, path, gradleTaskName, env as NodeJS.ProcessEnv, logger)
91+
return humanizeMavenStyleVersionRange(androidVersion)
92+
}

0 commit comments

Comments
 (0)