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
16 changes: 16 additions & 0 deletions .github/workflows/ci-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ jobs:
runs_ios: true,
runs_android: true,
},
{
package_type: 'module',
scenario: 'swift-cpp',
platforms: 'ios,android',
langs: 'swift,c++',
runs_ios: true,
runs_android: true,
},
{
package_type: 'module',
scenario: 'cpp-kotlin',
platforms: 'ios,android',
langs: 'c++,kotlin',
runs_ios: true,
runs_android: true,
},
{
package_type: 'module',
scenario: 'swift',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package com.$$androidNamespace$$;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.TurboReactPackage;
import com.facebook.react.BaseReactPackage;
import com.margelo.nitro.$$androidNamespace$$.$$androidCxxLibName$$OnLoad;


public class $$androidCxxLibName$$Package : TurboReactPackage() {
public class $$androidCxxLibName$$Package : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null

override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { emptyMap() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package com.$$androidNamespace$$;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.TurboReactPackage;
import com.facebook.react.BaseReactPackage;
import com.facebook.react.uimanager.ViewManager;
import com.margelo.nitro.$$androidNamespace$$.*;
import com.margelo.nitro.$$androidNamespace$$.views.*;


public class $$androidCxxLibName$$Package : TurboReactPackage() {
public class $$androidCxxLibName$$Package : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null

override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { emptyMap() }
Expand Down
4 changes: 2 additions & 2 deletions assets/template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@
"@semantic-release/git": "^10.0.1",
"@types/jest": "^29.5.12",
"@types/react": "19.2.0",
"nitrogen": "^0.35.0",
"nitrogen": "^0.35.2",
"react": "19.2.3",
"react-native": "0.84.1",
"react-native-builder-bob": "^0.40.18",
"react-native-nitro-modules": "^0.35.0",
"react-native-nitro-modules": "^0.35.2",
"conventional-changelog-conventionalcommits": "^9.1.0",
"semantic-release": "^25.0.3",
"typescript": "^5.8.3"
Expand Down
142 changes: 89 additions & 53 deletions src/cli/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import projectPackageJsonFile from '../../package.json'
import { generateInstructions, messages } from '../constants'
import { NitroModuleFactory } from '../generate-nitro-package'
import {
CreateModuleOptions,
type CreateModuleOptions,
Nitro,
PackageManager,
type PackageManager,
PLATFORM_LANGUAGE_MAP,
type PlatformLangMap,
SupportedLang,
SupportedPlatform,
UserAnswers,
type UserAnswers,
} from '../types'
import {
capitalize,
Expand Down Expand Up @@ -48,7 +49,13 @@ const getAllowedLanguageSelections = (
) {
return [
[SupportedLang.SWIFT, SupportedLang.KOTLIN],
...(packageType === Nitro.Module ? [[SupportedLang.CPP]] : []),
...(packageType === Nitro.Module
? [
[SupportedLang.CPP],
[SupportedLang.SWIFT, SupportedLang.CPP],
[SupportedLang.CPP, SupportedLang.KOTLIN],
]
: []),
]
}

Expand Down Expand Up @@ -89,7 +96,35 @@ const parsePlatformsOption = (value?: string) => {
: [SupportedPlatform.IOS, SupportedPlatform.ANDROID]
}

const parseLangsOption = (
const getPlatformLangMap = (
platforms: SupportedPlatform[],
langs: SupportedLang[]
): PlatformLangMap => {
const result: PlatformLangMap = {}

if (langs.length === 1) {
for (const platform of platforms) {
result[platform] = langs[0]
}
return result
}

if (langs.includes(SupportedLang.SWIFT)) {
result[SupportedPlatform.IOS] = SupportedLang.SWIFT
} else if (platforms.includes(SupportedPlatform.IOS)) {
result[SupportedPlatform.IOS] = SupportedLang.CPP
}

if (langs.includes(SupportedLang.KOTLIN)) {
result[SupportedPlatform.ANDROID] = SupportedLang.KOTLIN
} else if (platforms.includes(SupportedPlatform.ANDROID)) {
result[SupportedPlatform.ANDROID] = SupportedLang.CPP
}

return result
}

const parsePlatformLangsOption = (
value: string | undefined,
platforms: SupportedPlatform[],
packageType: Nitro
Expand All @@ -100,9 +135,12 @@ const parseLangsOption = (
)

if (!value) {
return packageType === Nitro.View
? resolveViewLanguages(platforms)
: allowedSelections[0]
return getPlatformLangMap(
platforms,
packageType === Nitro.View
? Object.values(resolveViewLanguages(platforms))
: allowedSelections[0]
)
}

const langs = Array.from(
Expand All @@ -129,7 +167,7 @@ const parseLangsOption = (
)
}

return langs
return getPlatformLangMap(platforms, langs)
}

export const createModule = async (
Expand Down Expand Up @@ -167,7 +205,7 @@ export const createModule = async (

moduleFactory = new NitroModuleFactory({
description: answers.description,
langs: answers.langs,
platformLangs: answers.platformLangs,
packageName,
platforms: answers.platforms,
pm: answers.pm,
Expand All @@ -181,7 +219,7 @@ export const createModule = async (

const modulePath = path.join(
process.cwd(),
'react-native-' + packageName.toLowerCase()
`react-native-${packageName.toLowerCase()}`
)
Comment on lines 220 to 223
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Use the configured output directory for existence checks and cleanup.

Line 220 and Line 273 hardcode process.cwd(). With --module-dir, this can check/delete the wrong folder (including unrelated directories) while generation runs elsewhere.

Proposed fix
 export const createModule = async (
     packageName: string,
     options: CreateModuleOptions
 ) => {
     let packageType = Nitro.Module
     let moduleFactory: NitroModuleFactory | null = null
     let spinnerStarted = false
+    let shouldCleanup = false
     const spinner = p.spinner()
+    const outputRoot = options.moduleDir || process.cwd()
     try {
@@
         moduleFactory = new NitroModuleFactory({
@@
-            cwd: options.moduleDir || process.cwd(),
+            cwd: outputRoot,
@@
         const modulePath = path.join(
-            process.cwd(),
+            outputRoot,
             `react-native-${packageName.toLowerCase()}`
         )
@@
         spinner.start(
             messages.creating.replace('{packageType}', capitalize(packageType))
         )
         spinnerStarted = true
+        shouldCleanup = true
 
         await moduleFactory.createNitroModule()
@@
     } catch (error) {
-        if (packageName) {
+        if (shouldCleanup && packageName) {
             const modulePath = path.join(
-                process.cwd(),
+                outputRoot,
                 `react-native-${packageName.toLowerCase()}`
             )
             rmSync(modulePath, { recursive: true, force: true })
         }

Also applies to: 273-274

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/create.ts` around lines 220 - 223, The code builds modulePath using
process.cwd() which ignores the configured output directory (--module-dir) and
can check/delete the wrong folder; update the path resolution so modulePath is
created from the resolved module directory option (the configured output
directory provided by the CLI, i.e. the --module-dir value) instead of
process.cwd(), and use that same resolved directory when doing existence checks
and cleanup (the places referencing modulePath/packageName and the cleanup logic
around lines 273-274) so all checks and deletions operate on the user-specified
output folder.

const dirExists = await dirExist(modulePath)

Expand Down Expand Up @@ -232,7 +270,7 @@ export const createModule = async (
if (packageName) {
const modulePath = path.join(
process.cwd(),
'react-native-' + packageName.toLowerCase()
`react-native-${packageName.toLowerCase()}`
)
rmSync(modulePath, { recursive: true, force: true })
}
Expand All @@ -253,55 +291,49 @@ export const createModule = async (
}
}

const selectLanguages = async (
const selectPlatformLanguages = async (
platforms: SupportedPlatform[],
packageType: Nitro
) => {
const options = getAllowedLanguageSelections(platforms, packageType).map(
langs => {
if (
langs.includes(SupportedLang.SWIFT) &&
langs.includes(SupportedLang.KOTLIN)
) {
return {
label: 'Swift & Kotlin',
value: langs,
hint: `Use Swift and Kotlin to build your Nitro ${packageType.toLowerCase()} for iOS and Android`,
}
}
): Promise<PlatformLangMap | symbol> => {
const result: PlatformLangMap = {}

const [lang] = langs
return {
label: capitalize(lang === SupportedLang.CPP ? 'c++' : lang),
value: langs,
hint:
lang === SupportedLang.CPP &&
platforms.includes(SupportedPlatform.IOS) &&
platforms.includes(SupportedPlatform.ANDROID)
? 'Use C++ to share code between iOS and Android'
: `Use ${lang === SupportedLang.CPP ? 'C++' : capitalize(lang)} to build your Nitro ${packageType.toLowerCase()} for ${platforms.join(' and ')}`,
}
for (const platform of platforms) {
const availableLanguages = PLATFORM_LANGUAGE_MAP[platform].filter(
lang => packageType !== Nitro.View || lang !== SupportedLang.CPP
)

if (availableLanguages.length === 1) {
result[platform] = availableLanguages[0]
continue
}
)

const selectedLangs = await p.select({
message: kleur.cyan('Which language(s) would you like to use?'),
options,
})
const selected = await p.select({
message: kleur.cyan(`Choose language for ${platform}:`),
options: availableLanguages.map(lang => ({
label: lang === SupportedLang.CPP ? 'C++' : capitalize(lang),
value: lang,
hint: `Use ${lang === SupportedLang.CPP ? 'C++' : capitalize(lang)} for ${platform}`,
})),
})

if (p.isCancel(selected)) return selected
result[platform] = selected
}

if (p.isCancel(selectedLangs)) return selectedLangs
return selectedLangs
return result
}

const resolveViewLanguages = (platforms: SupportedPlatform[]) => {
const langs = new Set<SupportedLang>()
const resolveViewLanguages = (
platforms: SupportedPlatform[]
): PlatformLangMap => {
const result: PlatformLangMap = {}
if (platforms.includes(SupportedPlatform.IOS)) {
langs.add(SupportedLang.SWIFT)
result[SupportedPlatform.IOS] = SupportedLang.SWIFT
}
if (platforms.includes(SupportedPlatform.ANDROID)) {
langs.add(SupportedLang.KOTLIN)
result[SupportedPlatform.ANDROID] = SupportedLang.KOTLIN
}
return Array.from(langs)
return result
}

const getUserAnswers = async (
Expand All @@ -318,7 +350,11 @@ const getUserAnswers = async (
description: `${kleur.yellow(`react-native-${name}`)} is a react native package built with Nitro`,
platforms,
packageType,
langs: parseLangsOption(options.langs, platforms, packageType),
platformLangs: parsePlatformLangsOption(
options.langs,
platforms,
packageType
),
pm: usedPm || 'pnpm',
}
}
Expand Down Expand Up @@ -393,14 +429,14 @@ const getUserAnswers = async (
initialValue: Nitro.Module,
})
},
langs: async ({ results }) => {
platformLangs: async ({ results }) => {
if (!results.platforms || !results.packageType) {
throw new Error('Missing required selections')
}
if (results.packageType === Nitro.View) {
return resolveViewLanguages(results.platforms)
}
return await selectLanguages(
return await selectPlatformLanguages(
results.platforms,
results.packageType
)
Expand Down Expand Up @@ -474,7 +510,7 @@ const getUserAnswers = async (
packageName: group.packageName,
packageType: group.packageType,
platforms: group.platforms,
langs: group.langs as SupportedLang[],
platformLangs: group.platformLangs as PlatformLangMap,
pm: group.pm,
description: group.description as string,
}
Expand Down
2 changes: 1 addition & 1 deletion src/code-snippets/code.js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const nitroModuleSpecCode = (
moduleName: string,
platformLang: string,
funcName: string
) => `import { type HybridObject } from 'react-native-nitro-modules'
) => `import type { HybridObject } from 'react-native-nitro-modules'

export interface ${toPascalCase(
moduleName
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Begin development:
${kleur.cyan('Implement native code:')}
${kleur.white('ios/')} ${kleur.dim('# iOS native implementation using swift')}
${kleur.white('android/')} ${kleur.dim('# Android native implementation using kotlin')}
${kleur.white('cpp/')} ${kleur.dim('# C++ native implementation. Shareable between iOS and Android (Will be generated if cpp was selected)')}
${kleur.white('cpp/')} ${kleur.dim('# C++ native implementation. Shareable between iOS and Android (Will be generated if c++ was selected)')}

${
skipExample
Expand Down
10 changes: 7 additions & 3 deletions src/file-generators/android-file-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import {
import { postScript } from '../code-snippets/code.js'
import { ANDROID_CXX_LIB_NAME_TAG, ANDROID_NAME_SPACE_TAG } from '../constants'
import {
FileGenerator,
GenerateModuleConfig,
type FileGenerator,
type GenerateModuleConfig,
Nitro,
SupportedLang,
SupportedPlatform,
} from '../types'
import {
createFolder,
Expand Down Expand Up @@ -71,7 +72,10 @@ export class AndroidFileGenerator implements FileGenerator {
)

// Only generate Kotlin file(s) if Kotlin is supported
if (config.langs.includes(SupportedLang.KOTLIN)) {
if (
config.platformLangs[SupportedPlatform.ANDROID] ===
SupportedLang.KOTLIN
) {
// Generate HybridObject file
const isHybridView = config.packageType === Nitro.View
await createModuleFile(
Expand Down
Loading
Loading