Skip to content
Open
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
29 changes: 29 additions & 0 deletions packages/create-react-native-brownfield/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# create-react-native-brownfield

Scaffolds React Native Brownfield packaging targets in an **existing** React Native Community CLI project (non-Expo).

## Usage

From inside your React Native app directory:

```bash
npx create-react-native-brownfield@latest
```

Or:

```bash
pnpm create react-native-brownfield
```

```bash
yarn create react-native-brownfield
```

## Options

- `--path <path>`: project root (default: `.`)
- `--ios-framework-name <name>`: iOS framework target name (default: `BrownfieldLib`)
- `--android-module-name <name>`: Android module name (default: `brownfieldlib`)
- `--debug`: verbose logs

4 changes: 4 additions & 0 deletions packages/create-react-native-brownfield/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import eslintRnConfig from '../../eslint.config.rn.mjs';

/** @type {import('eslint').Linter.Config[]} */
export default eslintRnConfig;
50 changes: 50 additions & 0 deletions packages/create-react-native-brownfield/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "create-react-native-brownfield",
"version": "0.0.0",
"license": "MIT",
"author": "Callstack",
"bin": "dist/main.js",
"type": "module",
"homepage": "https://github.com/callstack/react-native-brownfield",
"repository": {
"url": "git+https://github.com/callstack/react-native-brownfield.git"
},
"description": "Scaffold React Native Brownfield in an existing React Native CLI project",
"scripts": {
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.json",
"dev": "tsc -p tsconfig.json --watch"
},
"keywords": [
"react-native",
"brownfield",
"create",
"scaffold"
],
"files": [
"src",
"dist",
"!**/__tests__",
"!**/__fixtures__",
"!**/__mocks__",
"!**/.*",
"README.md"
],
"publishConfig": {
"access": "public"
},
"dependencies": {
"@callstack/react-native-brownfield": "workspace:^",
"commander": "^14.0.3"
},
"devDependencies": {
"@types/node": "^25.5.0",
"eslint": "^9.39.3",
"globals": "^17.3.0",
"typescript": "5.9.3"
},
"engines": {
"node": ">=20"
}
}
33 changes: 33 additions & 0 deletions packages/create-react-native-brownfield/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env node

import { Command } from 'commander';

import { scaffoldBrownfieldInRncCliProject } from '@callstack/react-native-brownfield/scaffold';

const program = new Command();

program
.name('create-react-native-brownfield')
.description(
'Scaffold React Native Brownfield packaging targets in an existing React Native CLI project.'
)
.option('-p, --path <path>', 'path to the React Native project', '.')
.option(
'--ios-framework-name <name>',
'iOS framework target name (default: BrownfieldLib)'
)
.option(
'--android-module-name <name>',
'Android library module name (default: brownfieldlib)'
)
.option('--debug', 'enable verbose logging', false)
.action(async (opts) => {
await scaffoldBrownfieldInRncCliProject({
projectRoot: opts.path,
iosFrameworkName: opts.iosFrameworkName,
androidModuleName: opts.androidModuleName,
debug: !!opts.debug,
});
});

program.parse(process.argv);
4 changes: 4 additions & 0 deletions packages/create-react-native-brownfield/src/xcode.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module 'xcode' {
const xcode: any;
export default xcode;
}
17 changes: 17 additions & 0 deletions packages/create-react-native-brownfield/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"lib": ["ES2022"],
"types": ["node"],
"declaration": true,
"sourceMap": true,
"verbatimModuleSyntax": false
},
"include": ["src/**/*.ts"]
}

16 changes: 15 additions & 1 deletion packages/react-native-brownfield/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@
"default": "./lib/commonjs/expo-config-plugin/app.plugin.js"
}
},
"./scaffold": {
"source": "./src/scaffold/index.ts",
"import": {
"types": "./lib/typescript/module/src/scaffold/index.d.ts",
"default": "./lib/module/scaffold/index.js"
},
"require": {
"types": "./lib/typescript/commonjs/src/scaffold/index.d.ts",
"default": "./lib/commonjs/scaffold/index.js"
}
},
"./package.json": "./package.json"
},
"scripts": {
Expand Down Expand Up @@ -86,7 +97,10 @@
"@expo/config-plugins": "^54.0.4"
},
"dependencies": {
"@callstack/brownfield-cli": "workspace:^"
"@callstack/brownfield-cli": "workspace:^",
"@react-native-community/cli-config": "^20.0.0",
"@react-native-community/cli-types": "^20.0.0",
"xcode": "^3.0.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,9 @@ export function createAndroidModule({
androidDir,
config,
rnVersion,
isExpoPre55,
templateVariant,
}: {
/**
* Whether the Expo project is pre-55
*/
isExpoPre55: boolean;
templateVariant: 'expo-pre55' | 'expo-post55' | 'vanilla';

/**
* The root Android directory path
Expand Down Expand Up @@ -78,9 +75,11 @@ export function createAndroidModule({
relativePath: `src/main/java/${config.android.packageName.replace(/\./g, '/')}/ReactNativeHostManager.kt`,
content: renderTemplate(
'android',
isExpoPre55
templateVariant === 'expo-pre55'
? 'ReactNativeHostManager.pre55.kt'
: 'ReactNativeHostManager.post55.kt',
: templateVariant === 'expo-post55'
? 'ReactNativeHostManager.post55.kt'
: 'ReactNativeHostManager.vanilla.kt',
{
'{{PACKAGE_NAME}}': android.packageName,
}
Expand Down Expand Up @@ -148,12 +147,13 @@ export const withAndroidModuleFiles: ConfigPlugin<
}

const { isExpoPre55 } = getExpoInfo(config);
const templateVariant = isExpoPre55 ? 'expo-pre55' : 'expo-post55';

createAndroidModule({
androidDir,
config: props,
rnVersion,
isExpoPre55,
templateVariant,
});

return dangerousConfig;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function ensureExpoDefinesForSDK55AndAbove(podfile: string): string {
export function modifyPodfile(
podfile: string,
frameworkName: string,
expoMajor: number
expoMajor?: number
): string {
// check if the framework target is already included
if (podfile.includes(`target '${frameworkName}'`)) {
Expand All @@ -111,9 +111,14 @@ export function modifyPodfile(
Logger.logDebug(`Modifying Podfile for framework: ${frameworkName}`);

// insert the framework target after the main target's "do"
const frameworkTargetBlock = renderTemplate('ios', 'PodfileTargetBlock.rb', {
'{{FRAMEWORK_NAME}}': frameworkName,
});
const useExpoHost = typeof expoMajor === 'number' && expoMajor >= 0;
const frameworkTargetBlock = renderTemplate(
'ios',
useExpoHost ? 'PodfileTargetBlock.rb' : 'PodfileTargetBlock.vanilla.rb',
{
'{{FRAMEWORK_NAME}}': frameworkName,
}
);

// find insertion point after the first target's content begins, before the end of the target block
const mainTargetMatch = podfile.match(
Expand All @@ -139,11 +144,13 @@ export function modifyPodfile(

Logger.logDebug(`Added framework target "${frameworkName}" to Podfile`);

if (expoMajor < 55) {
modifiedPodfile = ensureExpoPhaseOrderingHook(modifiedPodfile);
} else {
// Expo SDK >= 55
modifiedPodfile = ensureExpoDefinesForSDK55AndAbove(modifiedPodfile);
if (useExpoHost) {
if ((expoMajor as number) < 55) {
modifiedPodfile = ensureExpoPhaseOrderingHook(modifiedPodfile);
} else {
// Expo SDK >= 55
modifiedPodfile = ensureExpoDefinesForSDK55AndAbove(modifiedPodfile);
}
}

return modifiedPodfile;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export const withBrownfieldIos: ConfigPlugin<
const { frameworkTargetUUID, targetAlreadyExists } = addFrameworkTarget(
project,
modRequest,
props.ios
props.ios,
{ useExpoHost: true }
);

if (targetAlreadyExists) {
Expand Down Expand Up @@ -68,7 +69,9 @@ export const withBrownfieldIos: ConfigPlugin<
);
}

addSourceFilesBuildPhase(project, frameworkTargetUUID, props.ios);
addSourceFilesBuildPhase(project, frameworkTargetUUID, props.ios, {
useExpoHost: true,
});

return xcodeConfig;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,25 @@ import { renderTemplate } from '../template/engine';
* @returns The list of framework source files
*/
export function getFrameworkSourceFiles(
ios: ResolvedBrownfieldPluginConfigWithIos['ios']
ios: ResolvedBrownfieldPluginConfigWithIos['ios'],
options?: {
/**
* Whether the packaged framework is expected to use the Expo host.
* This influences template selection for the generated framework sources.
*/
useExpoHost?: boolean;
}
): RenderedTemplateFile[] {
const useExpoHost = options?.useExpoHost ?? true;

return [
{
relativePath: `${ios.frameworkName}.swift`,
content: renderTemplate('ios', 'FrameworkInterface.swift', {}),
content: renderTemplate(
'ios',
useExpoHost ? 'FrameworkInterface.swift' : 'FrameworkInterface.vanilla.swift',
{}
),
},
{
relativePath: 'Info.plist',
Expand All @@ -39,7 +52,8 @@ export function getFrameworkSourceFiles(
*/
export function createIosFramework(
iosDir: string,
config: ResolvedBrownfieldPluginConfigWithIos
config: ResolvedBrownfieldPluginConfigWithIos,
options?: Parameters<typeof getFrameworkSourceFiles>[1]
) {
const { ios } = config;
const frameworkDir = path.join(iosDir, ios.frameworkName);
Expand All @@ -61,7 +75,7 @@ export function createIosFramework(
}

// write files
for (const file of getFrameworkSourceFiles(ios)) {
for (const file of getFrameworkSourceFiles(ios, options)) {
const filePath = path.join(frameworkDir, file.relativePath);

fs.writeFileSync(filePath, file.content, 'utf8');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import { renderTemplate } from '../template/engine';
export function addFrameworkTarget(
project: XcodeProject,
modRequest: ModProps<XcodeProject>,
options: ResolvedBrownfieldPluginIosConfig
options: ResolvedBrownfieldPluginIosConfig,
brownfieldOptions?: {
useExpoHost?: boolean;
}
): {
frameworkTargetUUID: string;
targetAlreadyExists: boolean;
Expand Down Expand Up @@ -132,7 +135,7 @@ export function addFrameworkTarget(
});

// create the framework group in the project
const filePaths = getFrameworkSourceFiles(options).map(
const filePaths = getFrameworkSourceFiles(options, brownfieldOptions).map(
(file) => file.relativePath
);
const groupPath = path.join(modRequest.platformProjectRoot, frameworkName);
Expand Down Expand Up @@ -162,9 +165,12 @@ export function addFrameworkTarget(
export function addSourceFilesBuildPhase(
project: XcodeProject,
frameworkTargetUUID: string,
options: ResolvedBrownfieldPluginIosConfig
options: ResolvedBrownfieldPluginIosConfig,
brownfieldOptions?: {
useExpoHost?: boolean;
}
) {
const filePaths = getFrameworkSourceFiles(options).map(
const filePaths = getFrameworkSourceFiles(options, brownfieldOptions).map(
(file) => file.relativePath
);

Expand Down
Loading