@callstack/brownfield-cli 1.0.4 → 2.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/brownfield/commands/packageAndroid.d.ts.map +1 -1
  2. package/dist/brownfield/commands/packageAndroid.js +8 -1
  3. package/dist/brownfield/commands/packageIos.d.ts.map +1 -1
  4. package/dist/brownfield/commands/packageIos.js +23 -12
  5. package/dist/brownfield/commands/publishAndroid.d.ts.map +1 -1
  6. package/dist/brownfield/commands/publishAndroid.js +8 -1
  7. package/dist/brownfield/index.d.ts +0 -1
  8. package/dist/brownfield/index.d.ts.map +1 -1
  9. package/dist/brownfield/index.js +0 -1
  10. package/dist/brownfield/utils/expo.d.ts +7 -0
  11. package/dist/brownfield/utils/expo.d.ts.map +1 -0
  12. package/dist/brownfield/utils/expo.js +40 -0
  13. package/dist/brownfield/utils/paths.d.ts +6 -1
  14. package/dist/brownfield/utils/paths.d.ts.map +1 -1
  15. package/dist/brownfield/utils/paths.js +6 -4
  16. package/dist/brownfield/utils/project.d.ts +51 -0
  17. package/dist/brownfield/utils/project.d.ts.map +1 -0
  18. package/dist/brownfield/utils/project.js +114 -0
  19. package/dist/brownie/commands/codegen.d.ts.map +1 -1
  20. package/dist/brownie/commands/codegen.js +26 -11
  21. package/dist/brownie/errors/NoBrownieStoresError.d.ts +4 -0
  22. package/dist/brownie/errors/NoBrownieStoresError.d.ts.map +1 -0
  23. package/dist/brownie/errors/NoBrownieStoresError.js +6 -0
  24. package/dist/brownie/helpers/runBrownieCodegenIfApplicable.d.ts +5 -0
  25. package/dist/brownie/helpers/runBrownieCodegenIfApplicable.d.ts.map +1 -0
  26. package/dist/brownie/helpers/runBrownieCodegenIfApplicable.js +9 -0
  27. package/dist/brownie/store-discovery.d.ts.map +1 -1
  28. package/dist/brownie/store-discovery.js +2 -1
  29. package/package.json +11 -10
  30. package/src/brownfield/commands/packageAndroid.ts +9 -1
  31. package/src/brownfield/commands/packageIos.ts +28 -13
  32. package/src/brownfield/commands/publishAndroid.ts +9 -1
  33. package/src/brownfield/index.ts +0 -2
  34. package/src/brownfield/utils/expo.ts +71 -0
  35. package/src/brownfield/utils/paths.ts +7 -10
  36. package/src/brownfield/utils/project.ts +174 -0
  37. package/src/brownie/commands/codegen.ts +27 -14
  38. package/src/brownie/errors/NoBrownieStoresError.ts +6 -0
  39. package/src/brownie/helpers/runBrownieCodegenIfApplicable.ts +16 -0
  40. package/src/brownie/store-discovery.ts +3 -1
  41. package/CHANGELOG.md +0 -25
  42. package/src/brownfield/utils/index.ts +0 -3
  43. package/src/brownfield/utils/rn-cli.ts +0 -58
@@ -1 +1 @@
1
- {"version":3,"file":"packageAndroid.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/packageAndroid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,OAAO,EACL,YAAY,EAGb,MAAM,uBAAuB,CAAC;AAG/B,eAAO,MAAM,qBAAqB,SAkBjC,CAAC;AAEF,eAAO,MAAM,qBAAqB,cAGjC,CAAC"}
1
+ {"version":3,"file":"packageAndroid.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/packageAndroid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,OAAO,EACL,YAAY,EAGb,MAAM,uBAAuB,CAAC;AAK/B,eAAO,MAAM,qBAAqB,SAwBjC,CAAC;AAEF,eAAO,MAAM,qBAAqB,cAGjC,CAAC"}
@@ -2,11 +2,18 @@ import { Command } from 'commander';
2
2
  import { packageAarOptions, } from '@rock-js/platform-android';
3
3
  import { packageAarAction } from '@rock-js/plugin-brownfield-android';
4
4
  import { ExampleUsage, actionRunner, curryOptions, } from '../../shared/index.js';
5
- import { getProjectInfo } from '../utils/index.js';
5
+ import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
6
+ import { getProjectInfo } from '../utils/project.js';
7
+ import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
6
8
  export const packageAndroidCommand = curryOptions(new Command('package:android').description('Build Android AAR'), packageAarOptions.map((option) => option.name.startsWith('--variant')
7
9
  ? { ...option, default: 'debug' }
8
10
  : option)).action(actionRunner(async (options) => {
9
11
  const { projectRoot, platformConfig } = getProjectInfo('android');
12
+ await runExpoPrebuildIfNeeded({
13
+ projectRoot,
14
+ platform: 'android',
15
+ });
16
+ await runBrownieCodegenIfApplicable(projectRoot, 'kotlin');
10
17
  await packageAarAction({
11
18
  projectRoot,
12
19
  pluginConfig: platformConfig,
@@ -1 +1 @@
1
- {"version":3,"file":"packageIos.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/packageIos.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,OAAO,EAGL,YAAY,EACb,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,iBAAiB,SAuF7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,cAG7B,CAAC"}
1
+ {"version":3,"file":"packageIos.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/packageIos.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,OAAO,EAGL,YAAY,EACb,MAAM,uBAAuB,CAAC;AAI/B,eAAO,MAAM,iBAAiB,SAqG7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,cAG7B,CAAC"}
@@ -3,32 +3,43 @@ import { getBuildOptions, mergeFrameworks, } from '@rock-js/platform-apple-helpe
3
3
  import { packageIosAction } from '@rock-js/plugin-brownfield-ios';
4
4
  import { colorLink, getReactNativeVersion, logger, relativeToCwd, } from '@rock-js/tools';
5
5
  import { Command } from 'commander';
6
- import { isBrownieInstalled } from '../../brownie/config.js';
7
- import { runCodegen } from '../../brownie/commands/codegen.js';
8
- import { getProjectInfo, stripFrameworkBinary } from '../utils/index.js';
6
+ import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
7
+ import { getProjectInfo } from '../utils/project.js';
9
8
  import { actionRunner, curryOptions, ExampleUsage, } from '../../shared/index.js';
9
+ import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
10
+ import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
10
11
  export const packageIosCommand = curryOptions(new Command('package:ios').description('Build iOS XCFramework'), getBuildOptions({ platformName: 'ios' }).map((option) => option.name.startsWith('--build-folder')
11
12
  ? {
12
13
  ...option,
13
14
  description: option.description +
14
- " By default, the '<iOS project folder>/build' path will be used.",
15
+ " By default, the '.brownfield/build' path will be used.",
15
16
  }
16
17
  : option)).action(actionRunner(async (options) => {
17
18
  const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios');
19
+ await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' });
18
20
  if (!userConfig.project.ios) {
19
21
  throw new Error('iOS project not found.');
20
22
  }
21
23
  if (!userConfig.project.ios.xcodeProject) {
22
24
  throw new Error('iOS Xcode project not found in the configuration.');
23
25
  }
24
- const brownieCacheDir = path.join(userConfig.project.ios.sourceDir, '.brownfield');
25
- options.buildFolder ??= path.join(brownieCacheDir, 'build');
26
- const packageDir = path.join(brownieCacheDir, 'package');
27
- const configuration = options.configuration ?? 'Debug';
28
- const hasBrownie = isBrownieInstalled(projectRoot);
29
- if (hasBrownie) {
30
- await runCodegen({ platform: 'swift' });
26
+ let dotBrownfieldDir = path.join(
27
+ // for Expo projects, platformConfig?.sourceDir == "", but for non-Expo projects, it's "ios"
28
+ ...(userConfig.project.ios.sourceDir.trim().length > 0
29
+ ? [userConfig.project.ios.sourceDir]
30
+ : [projectRoot, 'ios']), '.brownfield');
31
+ // non-Expo projects have a relative sourceDir path, so we need to make it absolute
32
+ if (!path.isAbsolute(dotBrownfieldDir)) {
33
+ dotBrownfieldDir = path.join(projectRoot, dotBrownfieldDir);
31
34
  }
35
+ options.buildFolder ??= path.join(dotBrownfieldDir, 'build');
36
+ // The new_architecture.rb script scans Info.plist and fails on binary plist files,
37
+ // which is the case for our XCFrameworks.
38
+ // We're reusing the "build" directory which is excluded from the scan.
39
+ // Reference: https://github.com/facebook/react-native/blob/490c5e8dcc6cdb19c334cc39e93a39a48ba71e96/packages/react-native/scripts/cocoapods/new_architecture.rb#L171
40
+ const packageDir = path.join(dotBrownfieldDir, 'package', 'build');
41
+ const configuration = options.configuration ?? 'Debug';
42
+ const { hasBrownie } = await runBrownieCodegenIfApplicable(projectRoot, 'swift');
32
43
  await packageIosAction(options, {
33
44
  projectRoot,
34
45
  reactNativePath: userConfig.reactNativePath,
@@ -54,7 +65,7 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
54
65
  // Strip the binary from Brownie.xcframework to make it interface-only.
55
66
  // This avoids duplicate symbols when consumer apps embed both BrownfieldLib
56
67
  // (which contains Brownie symbols) and Brownie.xcframework.
57
- await stripFrameworkBinary(brownieOutputPath);
68
+ stripFrameworkBinary(brownieOutputPath);
58
69
  logger.success(`Brownie.xcframework created at ${colorLink(relativeToCwd(brownieOutputPath))}`);
59
70
  }
60
71
  }));
@@ -1 +1 @@
1
- {"version":3,"file":"publishAndroid.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/publishAndroid.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAGL,YAAY,EACb,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,qBAAqB,SAejC,CAAC;AAEF,eAAO,MAAM,qBAAqB,cAGjC,CAAC"}
1
+ {"version":3,"file":"publishAndroid.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/publishAndroid.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAGL,YAAY,EACb,MAAM,uBAAuB,CAAC;AAK/B,eAAO,MAAM,qBAAqB,SAqBjC,CAAC;AAEF,eAAO,MAAM,qBAAqB,cAGjC,CAAC"}
@@ -1,10 +1,17 @@
1
1
  import { publishLocalAarAction } from '@rock-js/plugin-brownfield-android';
2
2
  import { publishLocalAarOptions, } from '@rock-js/platform-android';
3
3
  import { Command } from 'commander';
4
- import { getProjectInfo } from '../utils/index.js';
5
4
  import { actionRunner, curryOptions, ExampleUsage, } from '../../shared/index.js';
5
+ import { getProjectInfo } from '../utils/project.js';
6
+ import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
7
+ import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
6
8
  export const publishAndroidCommand = curryOptions(new Command('publish:android').description('Publish Android package to Maven local'), publishLocalAarOptions).action(actionRunner(async (options) => {
7
9
  const { projectRoot, platformConfig } = getProjectInfo('android');
10
+ await runExpoPrebuildIfNeeded({
11
+ projectRoot,
12
+ platform: 'android',
13
+ });
14
+ await runBrownieCodegenIfApplicable(projectRoot, 'kotlin');
8
15
  await publishLocalAarAction({
9
16
  projectRoot,
10
17
  pluginConfig: platformConfig,
@@ -1,4 +1,3 @@
1
- export * from './utils/index.js';
2
1
  export declare const groupName: string;
3
2
  export declare const Commands: {
4
3
  packageAndroidCommand: import("commander").Command;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/brownfield/index.ts"],"names":[],"mappings":"AAWA,cAAc,kBAAkB,CAAC;AAEjC,eAAO,MAAM,SAAS,QAA+J,CAAC;AAEtL,eAAO,MAAM,QAAQ;;;;;;;CAOpB,CAAC;AACF,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/brownfield/index.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,SAAS,QAA+J,CAAC;AAEtL,eAAO,MAAM,QAAQ;;;;;;;CAOpB,CAAC;AACF,eAAe,QAAQ,CAAC"}
@@ -2,7 +2,6 @@ import { styleText } from 'node:util';
2
2
  import { packageAndroidCommand, packageAndroidExample, } from './commands/packageAndroid.js';
3
3
  import { publishAndroidCommand, publishAndroidExample, } from './commands/publishAndroid.js';
4
4
  import { packageIosCommand, packageIosExample } from './commands/packageIos.js';
5
- export * from './utils/index.js';
6
5
  export const groupName = `${styleText(['bold', 'blueBright'], '@callstack/react-native-brownfield')}${styleText('whiteBright', ' - utilities for React Native Brownfield projects')}`;
7
6
  export const Commands = {
8
7
  packageAndroidCommand,
@@ -0,0 +1,7 @@
1
+ type ExpoPlatform = 'ios' | 'android';
2
+ export declare function runExpoPrebuildIfNeeded({ projectRoot, platform, }: {
3
+ projectRoot: string;
4
+ platform: ExpoPlatform;
5
+ }): Promise<boolean>;
6
+ export {};
7
+ //# sourceMappingURL=expo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expo.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/expo.ts"],"names":[],"mappings":"AAMA,KAAK,YAAY,GAAG,KAAK,GAAG,SAAS,CAAC;AAqCtC,wBAAsB,uBAAuB,CAAC,EAC5C,WAAW,EACX,QAAQ,GACT,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,YAAY,CAAC;CACxB,oBAqBA"}
@@ -0,0 +1,40 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { logger } from '@rock-js/tools';
3
+ import { isExpoProject } from './project.js';
4
+ async function spawnCommand({ command, args, cwd, }) {
5
+ await new Promise((resolve, reject) => {
6
+ const child = spawn(command, args, {
7
+ cwd,
8
+ env: process.env,
9
+ stdio: 'inherit',
10
+ });
11
+ child.on('error', reject);
12
+ child.on('close', (code) => {
13
+ if (code === 0) {
14
+ resolve();
15
+ }
16
+ else {
17
+ reject(new Error(`Command "${command} ${args.join(' ')}" failed with exit code ${code ?? 'unknown'}.`));
18
+ }
19
+ });
20
+ });
21
+ }
22
+ function getNpxCommand() {
23
+ return process.platform === 'win32' ? 'npx.cmd' : 'npx';
24
+ }
25
+ export async function runExpoPrebuildIfNeeded({ projectRoot, platform, }) {
26
+ if (!isExpoProject(projectRoot)) {
27
+ return false;
28
+ }
29
+ logger.info(`Expo project detected. Running expo prebuild for ${platform}...`);
30
+ const args = ['expo', 'prebuild', '--platform', platform];
31
+ if (platform === 'ios') {
32
+ args.push('--no-install');
33
+ }
34
+ await spawnCommand({
35
+ command: getNpxCommand(),
36
+ args,
37
+ cwd: projectRoot,
38
+ });
39
+ return true;
40
+ }
@@ -1,5 +1,10 @@
1
1
  import type { AndroidProjectConfig, IOSProjectConfig } from '@react-native-community/cli-types';
2
- export declare function makeRelativeProjectConfigPaths<UserConfig extends AndroidProjectConfig | IOSProjectConfig | undefined>(projectRoot: string, userConfig: UserConfig): UserConfig;
2
+ /**
3
+ * Helper function to mutate the user config paths in place to be relative to the project root
4
+ * @param projectRoot The path to the project root directory
5
+ * @param userConfig User configuration from the RNC CLI
6
+ */
7
+ export declare function makeRelativeProjectConfigPaths<UserConfig extends AndroidProjectConfig | IOSProjectConfig | undefined>(projectRoot: string, userConfig: UserConfig): void;
3
8
  /**
4
9
  * Helper function to find RN project root by recursively looking for a package.json in the parent directories
5
10
  * @returns The path to the project root directory
@@ -1 +1 @@
1
- {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/paths.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,oBAAoB,EACpB,gBAAgB,EACjB,MAAM,mCAAmC,CAAC;AAG3C,wBAAgB,8BAA8B,CAC5C,UAAU,SAAS,oBAAoB,GAAG,gBAAgB,GAAG,SAAS,EACtE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,UAAU,CAWzD;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAWxC"}
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/paths.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,oBAAoB,EACpB,gBAAgB,EACjB,MAAM,mCAAmC,CAAC;AAE3C;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,SAAS,oBAAoB,GAAG,gBAAgB,GAAG,SAAS,EACtE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,QAI5C;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAWxC"}
@@ -1,12 +1,14 @@
1
1
  import * as path from 'node:path';
2
2
  import * as fs from 'node:fs';
3
- import cloneDeep from 'lodash.clonedeep';
3
+ /**
4
+ * Helper function to mutate the user config paths in place to be relative to the project root
5
+ * @param projectRoot The path to the project root directory
6
+ * @param userConfig User configuration from the RNC CLI
7
+ */
4
8
  export function makeRelativeProjectConfigPaths(projectRoot, userConfig) {
5
- const relativeConfig = cloneDeep(userConfig);
6
9
  if (userConfig?.sourceDir) {
7
- relativeConfig.sourceDir = path.relative(projectRoot, userConfig.sourceDir);
10
+ userConfig.sourceDir = path.relative(projectRoot, userConfig.sourceDir);
8
11
  }
9
- return relativeConfig;
10
12
  }
11
13
  /**
12
14
  * Helper function to find RN project root by recursively looking for a package.json in the parent directories
@@ -0,0 +1,51 @@
1
+ import type { AndroidProjectConfig, ProjectConfig, UserConfig } from '@react-native-community/cli-types';
2
+ import type { PackageAarFlags } from '@rock-js/platform-android';
3
+ import { type ProjectConfig as ExpoProjectConfig } from '@expo/config';
4
+ /**
5
+ * Gets the Expo config if the project is an Expo project
6
+ * @param projectRoot The project root path
7
+ * @returns The Expo config if the project is an Expo project, null otherwise
8
+ */
9
+ export declare function getExpoConfigIfIsExpo(projectRoot: string): ExpoProjectConfig | null;
10
+ /**
11
+ * Checks if the project is an Expo project; checks both if installed and explicitly listed
12
+ * in the project's package.json to prevent false positives in a monorepo setup
13
+ * @param projectRoot The project root path
14
+ * @returns Whether the project is an Expo project
15
+ */
16
+ export declare function isExpoProject(projectRoot: string): boolean;
17
+ /**
18
+ * Fills the RNC CLI project config from the Expo config by mutating the passed in `options.projectConfig` object in place
19
+ */
20
+ export declare function fillProjectConfigFromExpoConfig({ projectConfig, expoConfig: { exp }, projectRoot, }: {
21
+ /** The RNC CLI project config to be filled */
22
+ projectConfig: ProjectConfig;
23
+ /** The Expo project config */
24
+ expoConfig: ExpoProjectConfig;
25
+ /** The project root path */
26
+ projectRoot: string;
27
+ }): void;
28
+ /**
29
+ * Gets the project info for the given platform from the current working directory
30
+ * @param platform the platform for which to get project info
31
+ * @returns project root and android project config
32
+ */
33
+ export declare function getProjectInfo<Platform extends 'ios' | 'android'>(platform: Platform): {
34
+ projectRoot: string;
35
+ userConfig: UserConfig;
36
+ platformConfig: ProjectConfig[Platform];
37
+ };
38
+ export declare function getUserConfig({ projectRoot, platform, }: {
39
+ projectRoot: string;
40
+ platform: 'ios' | 'android';
41
+ }): UserConfig;
42
+ /**
43
+ * Gets the AAR packaging configuration for the given Android project
44
+ * @param args The AAR packaging flags
45
+ * @param androidConfig The Android project config
46
+ */
47
+ export declare function getAarConfig(args: PackageAarFlags, androidConfig: AndroidProjectConfig): {
48
+ sourceDir: string;
49
+ moduleName: string;
50
+ };
51
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/project.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,oBAAoB,EACpB,aAAa,EACb,UAAU,EACX,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAKjE,OAAO,EAEL,KAAK,aAAa,IAAI,iBAAiB,EACxC,MAAM,cAAc,CAAC;AAQtB;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,4BAMxD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAc1D;AAED;;GAEG;AACH,wBAAgB,+BAA+B,CAAC,EAC9C,aAAa,EACb,UAAU,EAAE,EAAE,GAAG,EAAE,EACnB,WAAW,GACZ,EAAE;IACD,8CAA8C;IAC9C,aAAa,EAAE,aAAa,CAAC;IAE7B,8BAA8B;IAC9B,UAAU,EAAE,iBAAiB,CAAC;IAE9B,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB,QAuBA;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,SAAS,KAAK,GAAG,SAAS,EAC/D,QAAQ,EAAE,QAAQ,GACjB;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;CACzC,CAeA;AAED,wBAAgB,aAAa,CAAC,EAC5B,WAAW,EACX,QAAQ,GACT,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAC;CAC7B,GAAG,UAAU,CAuBb;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,eAAe,EACrB,aAAa,EAAE,oBAAoB;;;EAQpC"}
@@ -0,0 +1,114 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import cliConfigImport from '@react-native-community/cli-config';
4
+ import { findProjectRoot, makeRelativeProjectConfigPaths } from './paths.js';
5
+ import { getConfig, } from '@expo/config';
6
+ const cliConfig = typeof cliConfigImport === 'function'
7
+ ? cliConfigImport
8
+ : // @ts-expect-error: interop default
9
+ cliConfigImport.default;
10
+ /**
11
+ * Gets the Expo config if the project is an Expo project
12
+ * @param projectRoot The project root path
13
+ * @returns The Expo config if the project is an Expo project, null otherwise
14
+ */
15
+ export function getExpoConfigIfIsExpo(projectRoot) {
16
+ try {
17
+ return getConfig(projectRoot, { skipSDKVersionRequirement: true });
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /**
24
+ * Checks if the project is an Expo project; checks both if installed and explicitly listed
25
+ * in the project's package.json to prevent false positives in a monorepo setup
26
+ * @param projectRoot The project root path
27
+ * @returns Whether the project is an Expo project
28
+ */
29
+ export function isExpoProject(projectRoot) {
30
+ const hasExpoConfig = getExpoConfigIfIsExpo(projectRoot) !== null;
31
+ // additionally, it is needed to check if the project depends on Expo packages explicitly
32
+ // to prevent false positives in a monorepo setup
33
+ const rnProjectRoot = findProjectRoot();
34
+ const packageJsonPath = path.join(rnProjectRoot, 'package.json');
35
+ const dependsOnExpo = fs.existsSync(packageJsonPath) &&
36
+ ['dependencies', 'peerDependencies', 'devDependencies'].some((key) => JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))[key]?.expo);
37
+ return hasExpoConfig && dependsOnExpo;
38
+ }
39
+ /**
40
+ * Fills the RNC CLI project config from the Expo config by mutating the passed in `options.projectConfig` object in place
41
+ */
42
+ export function fillProjectConfigFromExpoConfig({ projectConfig, expoConfig: { exp }, projectRoot, }) {
43
+ if (exp.android) {
44
+ projectConfig['android'] = {
45
+ applicationId: exp.android.package,
46
+ packageName: exp.android.package,
47
+ appName: exp.name,
48
+ assets: [],
49
+ mainActivity: 'MainActivity',
50
+ sourceDir: 'android',
51
+ };
52
+ }
53
+ if (exp.ios) {
54
+ projectConfig['ios'] = {
55
+ assets: [],
56
+ sourceDir: projectRoot,
57
+ xcodeProject: {
58
+ path: '.',
59
+ name: `${exp.name}.xcworkspace`,
60
+ isWorkspace: true,
61
+ },
62
+ };
63
+ }
64
+ }
65
+ /**
66
+ * Gets the project info for the given platform from the current working directory
67
+ * @param platform the platform for which to get project info
68
+ * @returns project root and android project config
69
+ */
70
+ export function getProjectInfo(platform) {
71
+ const projectRoot = findProjectRoot();
72
+ const userConfig = getUserConfig({ projectRoot, platform });
73
+ const platformConfig = userConfig.project[platform];
74
+ if (!platformConfig) {
75
+ throw new Error(`${platform} project not found.`);
76
+ }
77
+ return {
78
+ projectRoot,
79
+ userConfig,
80
+ platformConfig: platformConfig,
81
+ };
82
+ }
83
+ export function getUserConfig({ projectRoot, platform, }) {
84
+ // resolve the config using RNC CLI
85
+ const userConfig = cliConfig({
86
+ projectRoot,
87
+ selectedPlatform: platform,
88
+ });
89
+ let projectConfig = userConfig.project;
90
+ // below: try augmenting the config with values for Expo projects, if applicable
91
+ const maybeExpoConfig = getExpoConfigIfIsExpo(projectRoot);
92
+ if (maybeExpoConfig) {
93
+ fillProjectConfigFromExpoConfig({
94
+ projectConfig,
95
+ expoConfig: maybeExpoConfig,
96
+ projectRoot,
97
+ });
98
+ }
99
+ // below: relative sourceDir path is required by RN CLI's API
100
+ makeRelativeProjectConfigPaths(projectRoot, projectConfig[platform]);
101
+ return userConfig;
102
+ }
103
+ /**
104
+ * Gets the AAR packaging configuration for the given Android project
105
+ * @param args The AAR packaging flags
106
+ * @param androidConfig The Android project config
107
+ */
108
+ export function getAarConfig(args, androidConfig) {
109
+ const config = {
110
+ sourceDir: androidConfig.sourceDir,
111
+ moduleName: args.moduleName ?? '',
112
+ };
113
+ return config;
114
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../../../src/brownie/commands/codegen.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAa5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAqE5C,MAAM,MAAM,iBAAiB,GAAG;IAAE,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAAE,CAAC;AAExD;;GAEG;AACH,wBAAsB,UAAU,CAAC,EAAE,QAAQ,EAAE,EAAE,iBAAiB,iBAkC/D;AAED,eAAO,MAAM,cAAc,SAYxB,CAAC"}
1
+ {"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../../../src/brownie/commands/codegen.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAa5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAsE5C,MAAM,MAAM,iBAAiB,GAAG;IAAE,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAAE,CAAC;AAExD;;GAEG;AACH,wBAAsB,UAAU,CAAC,EAAE,QAAQ,EAAE,EAAE,iBAAiB,iBA8C/D;AAED,eAAO,MAAM,cAAc,SAYxB,CAAC"}
@@ -8,6 +8,7 @@ import { loadConfig, getSwiftOutputPath, } from '../config.js';
8
8
  import { generateSwift } from '../generators/swift.js';
9
9
  import { generateKotlin } from '../generators/kotlin.js';
10
10
  import { discoverStores } from '../store-discovery.js';
11
+ import { NoBrownieStoresError } from '../errors/NoBrownieStoresError.js';
11
12
  function getOutputPath(dir, name, ext) {
12
13
  return path.join(dir, `${name}.${ext}`);
13
14
  }
@@ -71,20 +72,34 @@ export async function runCodegen({ platform }) {
71
72
  logger.error(`Invalid platform: ${platform}. Must be 'swift' or 'kotlin'`);
72
73
  process.exit(1);
73
74
  }
74
- const stores = discoverStores();
75
- const isMultipleStores = stores.length > 1;
76
- const schemaList = stores.map((s) => path.basename(s.schemaPath)).join(', ');
77
- logger.info(styleText('cyan', `Generating store types from ${schemaList}...`));
78
- for (const store of stores) {
79
- let platforms;
80
- if (platform) {
81
- platforms = [platform];
75
+ try {
76
+ const stores = discoverStores();
77
+ const isMultipleStores = stores.length > 1;
78
+ const schemaList = stores
79
+ .map((s) => path.basename(s.schemaPath))
80
+ .join(', ');
81
+ logger.info(styleText('cyan', `Generating store types from ${schemaList}...`));
82
+ for (const store of stores) {
83
+ let platforms;
84
+ if (platform) {
85
+ platforms = [platform];
86
+ }
87
+ else {
88
+ // Only generate Swift by default (Kotlin not yet released)
89
+ platforms = ['swift'];
90
+ }
91
+ await generateForStore(store, config, platforms, isMultipleStores);
92
+ }
93
+ }
94
+ catch (error) {
95
+ if (error instanceof NoBrownieStoresError) {
96
+ logger.error(error.message);
97
+ outro('No brownie stores found, nothing was generated.');
98
+ return;
82
99
  }
83
100
  else {
84
- // Only generate Swift by default (Kotlin not yet released)
85
- platforms = ['swift'];
101
+ throw error;
86
102
  }
87
- await generateForStore(store, config, platforms, isMultipleStores);
88
103
  }
89
104
  outro('Done!');
90
105
  }
@@ -0,0 +1,4 @@
1
+ export declare class NoBrownieStoresError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ //# sourceMappingURL=NoBrownieStoresError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NoBrownieStoresError.d.ts","sourceRoot":"","sources":["../../../src/brownie/errors/NoBrownieStoresError.ts"],"names":[],"mappings":"AAAA,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B"}
@@ -0,0 +1,6 @@
1
+ export class NoBrownieStoresError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'NoBrownieStoresError';
5
+ }
6
+ }
@@ -0,0 +1,5 @@
1
+ import type { Platform } from '../types.js';
2
+ export declare function runBrownieCodegenIfApplicable(projectRoot: string, platform: Platform): Promise<{
3
+ hasBrownie: boolean;
4
+ }>;
5
+ //# sourceMappingURL=runBrownieCodegenIfApplicable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runBrownieCodegenIfApplicable.d.ts","sourceRoot":"","sources":["../../../src/brownie/helpers/runBrownieCodegenIfApplicable.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,wBAAsB,6BAA6B,CACjD,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,QAAQ;;GAQnB"}
@@ -0,0 +1,9 @@
1
+ import { runCodegen } from '../commands/codegen.js';
2
+ import { isBrownieInstalled } from '../config.js';
3
+ export async function runBrownieCodegenIfApplicable(projectRoot, platform) {
4
+ const hasBrownie = isBrownieInstalled(projectRoot);
5
+ if (hasBrownie) {
6
+ await runCodegen({ platform });
7
+ }
8
+ return { hasBrownie };
9
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"store-discovery.d.ts","sourceRoot":"","sources":["../../src/brownie/store-discovery.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AA4DD;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,GAAE,MAAsB,GAC9B,eAAe,EAAE,CAkCnB"}
1
+ {"version":3,"file":"store-discovery.d.ts","sourceRoot":"","sources":["../../src/brownie/store-discovery.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AA4DD;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,GAAE,MAAsB,GAC9B,eAAe,EAAE,CAkCnB"}
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Project } from 'ts-morph';
4
+ import { NoBrownieStoresError } from './errors/NoBrownieStoresError.js';
4
5
  /**
5
6
  * Recursively finds all *.brownie.ts files in a directory.
6
7
  */
@@ -53,7 +54,7 @@ function parseStoresFromFile(project, filePath) {
53
54
  export function discoverStores(rootDir = process.cwd()) {
54
55
  const brownieFiles = findBrownieFiles(rootDir);
55
56
  if (brownieFiles.length === 0) {
56
- throw new Error('No brownie store files found. Create a file ending with .brownie.ts ' +
57
+ throw new NoBrownieStoresError('No brownie store files found. Create a file ending with .brownie.ts ' +
57
58
  '(e.g., MyStore.brownie.ts)');
58
59
  }
59
60
  const project = new Project({ skipAddingFilesFromTsConfig: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callstack/brownfield-cli",
3
- "version": "1.0.4",
3
+ "version": "2.0.0-rc.2",
4
4
  "license": "MIT",
5
5
  "author": "Artur Morys-Magiera <artus9033@gmail.com>",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  ],
14
14
  "homepage": "https://github.com/callstack/react-native-brownfield",
15
15
  "repository": {
16
- "url": "https://github.com/callstack/react-native-brownfield"
16
+ "url": "git+https://github.com/callstack/react-native-brownfield.git"
17
17
  },
18
18
  "description": "Brownfield CLI for React Native, gathering all packages of the RN brownfield ecosystem",
19
19
  "exports": {
@@ -67,15 +67,17 @@
67
67
  "@react-native-community/cli-config-android": "*"
68
68
  },
69
69
  "dependencies": {
70
+ "@expo/config": "^12.0.13",
70
71
  "@react-native-community/cli-config": "^20.0.0",
71
72
  "@react-native-community/cli-config-android": "^20.0.0",
72
- "@rock-js/platform-android": "^0.12.6",
73
- "@rock-js/platform-apple-helpers": "^0.12.6",
74
- "@rock-js/plugin-brownfield-android": "^0.12.6",
75
- "@rock-js/plugin-brownfield-ios": "^0.12.6",
76
- "@rock-js/tools": "^0.12.6",
73
+ "@rock-js/platform-android": "^0.12.8",
74
+ "@rock-js/platform-apple-helpers": "^0.12.8",
75
+ "@rock-js/plugin-brownfield-android": "^0.12.8",
76
+ "@rock-js/plugin-brownfield-ios": "^0.12.8",
77
+ "@rock-js/tools": "^0.12.8",
77
78
  "commander": "^14.0.2",
78
- "lodash.clonedeep": "^4.5.0",
79
+ "quicktype-core": "^23.2.6",
80
+ "quicktype-typescript-input": "^23.2.6",
79
81
  "ts-morph": "^27.0.2"
80
82
  },
81
83
  "devDependencies": {
@@ -87,7 +89,6 @@
87
89
  "@react-native/eslint-config": "0.82.1",
88
90
  "@types/babel__core": "^7.20.5",
89
91
  "@types/babel__preset-env": "^7.10.0",
90
- "@types/lodash.clonedeep": "^4.5.9",
91
92
  "@types/node": "^25.0.8",
92
93
  "@vitest/coverage-v8": "^4.0.17",
93
94
  "eslint": "^9.28.0",
@@ -99,4 +100,4 @@
99
100
  "engines": {
100
101
  "node": ">=20"
101
102
  }
102
- }
103
+ }
@@ -11,7 +11,9 @@ import {
11
11
  actionRunner,
12
12
  curryOptions,
13
13
  } from '../../shared/index.js';
14
- import { getProjectInfo } from '../utils/index.js';
14
+ import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
15
+ import { getProjectInfo } from '../utils/project.js';
16
+ import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
15
17
 
16
18
  export const packageAndroidCommand = curryOptions(
17
19
  new Command('package:android').description('Build Android AAR'),
@@ -23,6 +25,12 @@ export const packageAndroidCommand = curryOptions(
23
25
  ).action(
24
26
  actionRunner(async (options: PackageAarFlags) => {
25
27
  const { projectRoot, platformConfig } = getProjectInfo('android');
28
+ await runExpoPrebuildIfNeeded({
29
+ projectRoot,
30
+ platform: 'android',
31
+ });
32
+
33
+ await runBrownieCodegenIfApplicable(projectRoot, 'kotlin');
26
34
 
27
35
  await packageAarAction({
28
36
  projectRoot,
@@ -15,14 +15,15 @@ import {
15
15
 
16
16
  import { Command } from 'commander';
17
17
 
18
- import { isBrownieInstalled } from '../../brownie/config.js';
19
- import { runCodegen } from '../../brownie/commands/codegen.js';
20
- import { getProjectInfo, stripFrameworkBinary } from '../utils/index.js';
18
+ import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
19
+ import { getProjectInfo } from '../utils/project.js';
21
20
  import {
22
21
  actionRunner,
23
22
  curryOptions,
24
23
  ExampleUsage,
25
24
  } from '../../shared/index.js';
25
+ import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
26
+ import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
26
27
 
27
28
  export const packageIosCommand = curryOptions(
28
29
  new Command('package:ios').description('Build iOS XCFramework'),
@@ -32,13 +33,14 @@ export const packageIosCommand = curryOptions(
32
33
  ...option,
33
34
  description:
34
35
  option.description +
35
- " By default, the '<iOS project folder>/build' path will be used.",
36
+ " By default, the '.brownfield/build' path will be used.",
36
37
  }
37
38
  : option
38
39
  )
39
40
  ).action(
40
41
  actionRunner(async (options: AppleBuildFlags) => {
41
42
  const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios');
43
+ await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' });
42
44
 
43
45
  if (!userConfig.project.ios) {
44
46
  throw new Error('iOS project not found.');
@@ -48,19 +50,32 @@ export const packageIosCommand = curryOptions(
48
50
  throw new Error('iOS Xcode project not found in the configuration.');
49
51
  }
50
52
 
51
- const brownieCacheDir = path.join(
52
- userConfig.project.ios.sourceDir,
53
+ let dotBrownfieldDir = path.join(
54
+ // for Expo projects, platformConfig?.sourceDir == "", but for non-Expo projects, it's "ios"
55
+ ...(userConfig.project.ios.sourceDir.trim().length > 0
56
+ ? [userConfig.project.ios.sourceDir]
57
+ : [projectRoot, 'ios']),
53
58
  '.brownfield'
54
59
  );
55
60
 
56
- options.buildFolder ??= path.join(brownieCacheDir, 'build');
57
- const packageDir = path.join(brownieCacheDir, 'package');
61
+ // non-Expo projects have a relative sourceDir path, so we need to make it absolute
62
+ if (!path.isAbsolute(dotBrownfieldDir)) {
63
+ dotBrownfieldDir = path.join(projectRoot, dotBrownfieldDir);
64
+ }
65
+
66
+ options.buildFolder ??= path.join(dotBrownfieldDir, 'build');
67
+
68
+ // The new_architecture.rb script scans Info.plist and fails on binary plist files,
69
+ // which is the case for our XCFrameworks.
70
+ // We're reusing the "build" directory which is excluded from the scan.
71
+ // Reference: https://github.com/facebook/react-native/blob/490c5e8dcc6cdb19c334cc39e93a39a48ba71e96/packages/react-native/scripts/cocoapods/new_architecture.rb#L171
72
+ const packageDir = path.join(dotBrownfieldDir, 'package', 'build');
58
73
  const configuration = options.configuration ?? 'Debug';
59
74
 
60
- const hasBrownie = isBrownieInstalled(projectRoot);
61
- if (hasBrownie) {
62
- await runCodegen({ platform: 'swift' });
63
- }
75
+ const { hasBrownie } = await runBrownieCodegenIfApplicable(
76
+ projectRoot,
77
+ 'swift'
78
+ );
64
79
 
65
80
  await packageIosAction(
66
81
  options,
@@ -104,7 +119,7 @@ export const packageIosCommand = curryOptions(
104
119
  // Strip the binary from Brownie.xcframework to make it interface-only.
105
120
  // This avoids duplicate symbols when consumer apps embed both BrownfieldLib
106
121
  // (which contains Brownie symbols) and Brownie.xcframework.
107
- await stripFrameworkBinary(brownieOutputPath);
122
+ stripFrameworkBinary(brownieOutputPath);
108
123
 
109
124
  logger.success(
110
125
  `Brownie.xcframework created at ${colorLink(relativeToCwd(brownieOutputPath))}`
@@ -6,12 +6,14 @@ import {
6
6
 
7
7
  import { Command } from 'commander';
8
8
 
9
- import { getProjectInfo } from '../utils/index.js';
10
9
  import {
11
10
  actionRunner,
12
11
  curryOptions,
13
12
  ExampleUsage,
14
13
  } from '../../shared/index.js';
14
+ import { getProjectInfo } from '../utils/project.js';
15
+ import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
16
+ import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
15
17
 
16
18
  export const publishAndroidCommand = curryOptions(
17
19
  new Command('publish:android').description(
@@ -21,6 +23,12 @@ export const publishAndroidCommand = curryOptions(
21
23
  ).action(
22
24
  actionRunner(async (options: PublishLocalAarFlags) => {
23
25
  const { projectRoot, platformConfig } = getProjectInfo('android');
26
+ await runExpoPrebuildIfNeeded({
27
+ projectRoot,
28
+ platform: 'android',
29
+ });
30
+
31
+ await runBrownieCodegenIfApplicable(projectRoot, 'kotlin');
24
32
 
25
33
  await publishLocalAarAction({
26
34
  projectRoot,
@@ -9,8 +9,6 @@ import {
9
9
  } from './commands/publishAndroid.js';
10
10
  import { packageIosCommand, packageIosExample } from './commands/packageIos.js';
11
11
 
12
- export * from './utils/index.js';
13
-
14
12
  export const groupName = `${styleText(['bold', 'blueBright'], '@callstack/react-native-brownfield')}${styleText('whiteBright', ' - utilities for React Native Brownfield projects')}`;
15
13
 
16
14
  export const Commands = {
@@ -0,0 +1,71 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ import { logger } from '@rock-js/tools';
4
+
5
+ import { isExpoProject } from './project.js';
6
+
7
+ type ExpoPlatform = 'ios' | 'android';
8
+
9
+ async function spawnCommand({
10
+ command,
11
+ args,
12
+ cwd,
13
+ }: {
14
+ command: string;
15
+ args: string[];
16
+ cwd: string;
17
+ }) {
18
+ await new Promise<void>((resolve, reject) => {
19
+ const child = spawn(command, args, {
20
+ cwd,
21
+ env: process.env,
22
+ stdio: 'inherit',
23
+ });
24
+
25
+ child.on('error', reject);
26
+ child.on('close', (code) => {
27
+ if (code === 0) {
28
+ resolve();
29
+ } else {
30
+ reject(
31
+ new Error(
32
+ `Command "${command} ${args.join(' ')}" failed with exit code ${code ?? 'unknown'}.`
33
+ )
34
+ );
35
+ }
36
+ });
37
+ });
38
+ }
39
+
40
+ function getNpxCommand() {
41
+ return process.platform === 'win32' ? 'npx.cmd' : 'npx';
42
+ }
43
+
44
+ export async function runExpoPrebuildIfNeeded({
45
+ projectRoot,
46
+ platform,
47
+ }: {
48
+ projectRoot: string;
49
+ platform: ExpoPlatform;
50
+ }) {
51
+ if (!isExpoProject(projectRoot)) {
52
+ return false;
53
+ }
54
+
55
+ logger.info(
56
+ `Expo project detected. Running expo prebuild for ${platform}...`
57
+ );
58
+
59
+ const args = ['expo', 'prebuild', '--platform', platform];
60
+ if (platform === 'ios') {
61
+ args.push('--no-install');
62
+ }
63
+
64
+ await spawnCommand({
65
+ command: getNpxCommand(),
66
+ args,
67
+ cwd: projectRoot,
68
+ });
69
+
70
+ return true;
71
+ }
@@ -5,21 +5,18 @@ import type {
5
5
  AndroidProjectConfig,
6
6
  IOSProjectConfig,
7
7
  } from '@react-native-community/cli-types';
8
- import cloneDeep from 'lodash.clonedeep';
9
8
 
9
+ /**
10
+ * Helper function to mutate the user config paths in place to be relative to the project root
11
+ * @param projectRoot The path to the project root directory
12
+ * @param userConfig User configuration from the RNC CLI
13
+ */
10
14
  export function makeRelativeProjectConfigPaths<
11
15
  UserConfig extends AndroidProjectConfig | IOSProjectConfig | undefined,
12
- >(projectRoot: string, userConfig: UserConfig): UserConfig {
13
- const relativeConfig = cloneDeep(userConfig);
14
-
16
+ >(projectRoot: string, userConfig: UserConfig) {
15
17
  if (userConfig?.sourceDir) {
16
- relativeConfig!.sourceDir = path.relative(
17
- projectRoot,
18
- userConfig.sourceDir
19
- );
18
+ userConfig.sourceDir = path.relative(projectRoot, userConfig.sourceDir);
20
19
  }
21
-
22
- return relativeConfig;
23
20
  }
24
21
 
25
22
  /**
@@ -0,0 +1,174 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import type {
4
+ AndroidProjectConfig,
5
+ ProjectConfig,
6
+ UserConfig,
7
+ } from '@react-native-community/cli-types';
8
+ import type { PackageAarFlags } from '@rock-js/platform-android';
9
+
10
+ import cliConfigImport from '@react-native-community/cli-config';
11
+
12
+ import { findProjectRoot, makeRelativeProjectConfigPaths } from './paths.js';
13
+ import {
14
+ getConfig,
15
+ type ProjectConfig as ExpoProjectConfig,
16
+ } from '@expo/config';
17
+
18
+ const cliConfig: typeof cliConfigImport =
19
+ typeof cliConfigImport === 'function'
20
+ ? cliConfigImport
21
+ : // @ts-expect-error: interop default
22
+ cliConfigImport.default;
23
+
24
+ /**
25
+ * Gets the Expo config if the project is an Expo project
26
+ * @param projectRoot The project root path
27
+ * @returns The Expo config if the project is an Expo project, null otherwise
28
+ */
29
+ export function getExpoConfigIfIsExpo(projectRoot: string) {
30
+ try {
31
+ return getConfig(projectRoot, { skipSDKVersionRequirement: true });
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Checks if the project is an Expo project; checks both if installed and explicitly listed
39
+ * in the project's package.json to prevent false positives in a monorepo setup
40
+ * @param projectRoot The project root path
41
+ * @returns Whether the project is an Expo project
42
+ */
43
+ export function isExpoProject(projectRoot: string): boolean {
44
+ const hasExpoConfig = getExpoConfigIfIsExpo(projectRoot) !== null;
45
+
46
+ // additionally, it is needed to check if the project depends on Expo packages explicitly
47
+ // to prevent false positives in a monorepo setup
48
+ const rnProjectRoot = findProjectRoot();
49
+ const packageJsonPath = path.join(rnProjectRoot, 'package.json');
50
+ const dependsOnExpo =
51
+ fs.existsSync(packageJsonPath) &&
52
+ ['dependencies', 'peerDependencies', 'devDependencies'].some(
53
+ (key) => JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))[key]?.expo
54
+ );
55
+
56
+ return hasExpoConfig && dependsOnExpo;
57
+ }
58
+
59
+ /**
60
+ * Fills the RNC CLI project config from the Expo config by mutating the passed in `options.projectConfig` object in place
61
+ */
62
+ export function fillProjectConfigFromExpoConfig({
63
+ projectConfig,
64
+ expoConfig: { exp },
65
+ projectRoot,
66
+ }: {
67
+ /** The RNC CLI project config to be filled */
68
+ projectConfig: ProjectConfig;
69
+
70
+ /** The Expo project config */
71
+ expoConfig: ExpoProjectConfig;
72
+
73
+ /** The project root path */
74
+ projectRoot: string;
75
+ }) {
76
+ if (exp.android) {
77
+ projectConfig['android'] = {
78
+ applicationId: exp.android.package!,
79
+ packageName: exp.android.package!,
80
+ appName: exp.name!,
81
+ assets: [],
82
+ mainActivity: 'MainActivity',
83
+ sourceDir: 'android',
84
+ };
85
+ }
86
+
87
+ if (exp.ios) {
88
+ projectConfig['ios'] = {
89
+ assets: [],
90
+ sourceDir: projectRoot,
91
+ xcodeProject: {
92
+ path: '.',
93
+ name: `${exp.name}.xcworkspace`,
94
+ isWorkspace: true,
95
+ },
96
+ };
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Gets the project info for the given platform from the current working directory
102
+ * @param platform the platform for which to get project info
103
+ * @returns project root and android project config
104
+ */
105
+ export function getProjectInfo<Platform extends 'ios' | 'android'>(
106
+ platform: Platform
107
+ ): {
108
+ projectRoot: string;
109
+ userConfig: UserConfig;
110
+ platformConfig: ProjectConfig[Platform];
111
+ } {
112
+ const projectRoot = findProjectRoot();
113
+
114
+ const userConfig = getUserConfig({ projectRoot, platform });
115
+ const platformConfig = userConfig.project[platform as Platform];
116
+
117
+ if (!platformConfig) {
118
+ throw new Error(`${platform} project not found.`);
119
+ }
120
+
121
+ return {
122
+ projectRoot,
123
+ userConfig,
124
+ platformConfig: platformConfig,
125
+ };
126
+ }
127
+
128
+ export function getUserConfig({
129
+ projectRoot,
130
+ platform,
131
+ }: {
132
+ projectRoot: string;
133
+ platform: 'ios' | 'android';
134
+ }): UserConfig {
135
+ // resolve the config using RNC CLI
136
+ const userConfig = cliConfig({
137
+ projectRoot,
138
+ selectedPlatform: platform,
139
+ });
140
+
141
+ let projectConfig = userConfig.project;
142
+
143
+ // below: try augmenting the config with values for Expo projects, if applicable
144
+ const maybeExpoConfig = getExpoConfigIfIsExpo(projectRoot);
145
+ if (maybeExpoConfig) {
146
+ fillProjectConfigFromExpoConfig({
147
+ projectConfig,
148
+ expoConfig: maybeExpoConfig,
149
+ projectRoot,
150
+ });
151
+ }
152
+
153
+ // below: relative sourceDir path is required by RN CLI's API
154
+ makeRelativeProjectConfigPaths(projectRoot, projectConfig[platform]);
155
+
156
+ return userConfig;
157
+ }
158
+
159
+ /**
160
+ * Gets the AAR packaging configuration for the given Android project
161
+ * @param args The AAR packaging flags
162
+ * @param androidConfig The Android project config
163
+ */
164
+ export function getAarConfig(
165
+ args: PackageAarFlags,
166
+ androidConfig: AndroidProjectConfig
167
+ ) {
168
+ const config = {
169
+ sourceDir: androidConfig.sourceDir,
170
+ moduleName: args.moduleName ?? '',
171
+ };
172
+
173
+ return config;
174
+ }
@@ -15,6 +15,7 @@ import { generateSwift } from '../generators/swift.js';
15
15
  import { generateKotlin } from '../generators/kotlin.js';
16
16
  import { discoverStores, type DiscoveredStore } from '../store-discovery.js';
17
17
  import type { Platform } from '../types.js';
18
+ import { NoBrownieStoresError } from '../errors/NoBrownieStoresError.js';
18
19
 
19
20
  function getOutputPath(dir: string, name: string, ext: string): string {
20
21
  return path.join(dir, `${name}.${ext}`);
@@ -100,25 +101,37 @@ export async function runCodegen({ platform }: RunCodegenOptions) {
100
101
  process.exit(1);
101
102
  }
102
103
 
103
- const stores = discoverStores();
104
- const isMultipleStores = stores.length > 1;
105
- const schemaList = stores.map((s) => path.basename(s.schemaPath)).join(', ');
104
+ try {
105
+ const stores = discoverStores();
106
+ const isMultipleStores = stores.length > 1;
107
+ const schemaList = stores
108
+ .map((s) => path.basename(s.schemaPath))
109
+ .join(', ');
106
110
 
107
- logger.info(
108
- styleText('cyan', `Generating store types from ${schemaList}...`)
109
- );
111
+ logger.info(
112
+ styleText('cyan', `Generating store types from ${schemaList}...`)
113
+ );
114
+
115
+ for (const store of stores) {
116
+ let platforms: Platform[];
110
117
 
111
- for (const store of stores) {
112
- let platforms: Platform[];
118
+ if (platform) {
119
+ platforms = [platform];
120
+ } else {
121
+ // Only generate Swift by default (Kotlin not yet released)
122
+ platforms = ['swift'];
123
+ }
113
124
 
114
- if (platform) {
115
- platforms = [platform];
125
+ await generateForStore(store, config, platforms, isMultipleStores);
126
+ }
127
+ } catch (error) {
128
+ if (error instanceof NoBrownieStoresError) {
129
+ logger.error(error.message);
130
+ outro('No brownie stores found, nothing was generated.');
131
+ return;
116
132
  } else {
117
- // Only generate Swift by default (Kotlin not yet released)
118
- platforms = ['swift'];
133
+ throw error;
119
134
  }
120
-
121
- await generateForStore(store, config, platforms, isMultipleStores);
122
135
  }
123
136
 
124
137
  outro('Done!');
@@ -0,0 +1,6 @@
1
+ export class NoBrownieStoresError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = 'NoBrownieStoresError';
5
+ }
6
+ }
@@ -0,0 +1,16 @@
1
+ import { runCodegen } from '../commands/codegen.js';
2
+ import { isBrownieInstalled } from '../config.js';
3
+
4
+ import type { Platform } from '../types.js';
5
+
6
+ export async function runBrownieCodegenIfApplicable(
7
+ projectRoot: string,
8
+ platform: Platform
9
+ ) {
10
+ const hasBrownie = isBrownieInstalled(projectRoot);
11
+ if (hasBrownie) {
12
+ await runCodegen({ platform });
13
+ }
14
+
15
+ return { hasBrownie };
16
+ }
@@ -2,6 +2,8 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Project } from 'ts-morph';
4
4
 
5
+ import { NoBrownieStoresError } from './errors/NoBrownieStoresError.js';
6
+
5
7
  export interface DiscoveredStore {
6
8
  name: string;
7
9
  schemaPath: string;
@@ -75,7 +77,7 @@ export function discoverStores(
75
77
  const brownieFiles = findBrownieFiles(rootDir);
76
78
 
77
79
  if (brownieFiles.length === 0) {
78
- throw new Error(
80
+ throw new NoBrownieStoresError(
79
81
  'No brownie store files found. Create a file ending with .brownie.ts ' +
80
82
  '(e.g., MyStore.brownie.ts)'
81
83
  );
package/CHANGELOG.md DELETED
@@ -1,25 +0,0 @@
1
- # @callstack/brownfield-cli
2
-
3
- ## 1.0.4
4
-
5
- ### Patch Changes
6
-
7
- - [#216](https://github.com/callstack/react-native-brownfield/pull/216) [`8ce3ea1`](https://github.com/callstack/react-native-brownfield/commit/8ce3ea10e0719adac7396dea8f171753e901b31d) Thanks [@thymikee](https://github.com/thymikee)! - chore: remove release-it
8
-
9
- ## 1.0.3
10
-
11
- ### Patch Changes
12
-
13
- - [#213](https://github.com/callstack/react-native-brownfield/pull/213) [`2347775`](https://github.com/callstack/react-native-brownfield/commit/23477753b16ee189b82c1aee3eac98a56c79f52a) Thanks [@thymikee](https://github.com/thymikee)! - feat: create brownfield package as CLI proxy
14
-
15
- ## 1.0.2
16
-
17
- ### Patch Changes
18
-
19
- - [`2a8563f`](https://github.com/callstack/react-native-brownfield/commit/2a8563f65ed152054ad1290caf963791a368ee9a) Thanks [@okwasniewski](https://github.com/okwasniewski)! - feat: strip framework binaries to avoid duplicate symbol errors
20
-
21
- ## 1.0.1
22
-
23
- ### Patch Changes
24
-
25
- - [#198](https://github.com/callstack/react-native-brownfield/pull/198) [`c8c903d`](https://github.com/callstack/react-native-brownfield/commit/c8c903d0d2b78a8c06a41213dfbe781a2daf3d25) Thanks [@artus9033](https://github.com/artus9033)! - docs: added README files to all packages
@@ -1,3 +0,0 @@
1
- export * from './paths.js';
2
- export * from './rn-cli.js';
3
- export * from './stripFrameworkBinary.js';
@@ -1,58 +0,0 @@
1
- import type { PackageAarFlags } from '@rock-js/platform-android';
2
- import type {
3
- AndroidProjectConfig,
4
- Config as UserConfig,
5
- ProjectConfig,
6
- } from '@react-native-community/cli-types';
7
- import cliConfigImport from '@react-native-community/cli-config';
8
-
9
- const cliConfig: typeof cliConfigImport =
10
- typeof cliConfigImport === 'function'
11
- ? cliConfigImport
12
- : // @ts-expect-error: interop default
13
- cliConfigImport.default;
14
-
15
- import { findProjectRoot, makeRelativeProjectConfigPaths } from './paths.js';
16
-
17
- /**
18
- * Gets the project info for the given platform from the current working directory
19
- * @param platform the platform for which to get project info
20
- * @returns project root and android project config
21
- */
22
- export function getProjectInfo<Platform extends 'ios' | 'android'>(
23
- platform: Platform
24
- ): {
25
- projectRoot: string;
26
- userConfig: UserConfig;
27
- platformConfig: ProjectConfig[Platform];
28
- } {
29
- const projectRoot = findProjectRoot();
30
-
31
- const userConfig = cliConfig({
32
- projectRoot,
33
- selectedPlatform: platform,
34
- });
35
-
36
- // below: relative sourceDir path is required by RN CLI's API
37
- const platformConfig = makeRelativeProjectConfigPaths(
38
- projectRoot,
39
- userConfig.project[platform]
40
- );
41
-
42
- if (!platformConfig) {
43
- throw new Error(`${platform} project not found.`);
44
- }
45
-
46
- return { projectRoot, userConfig, platformConfig };
47
- }
48
-
49
- export const getAarConfig = (
50
- args: PackageAarFlags,
51
- androidConfig: AndroidProjectConfig
52
- ) => {
53
- const config = {
54
- sourceDir: androidConfig.sourceDir,
55
- moduleName: args.moduleName ?? '',
56
- };
57
- return config;
58
- };