@callstack/brownfield-cli 3.11.0 → 3.13.0

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 (48) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/brownfield/commands/packageAndroid.d.ts.map +1 -1
  3. package/dist/brownfield/commands/packageAndroid.js +3 -3
  4. package/dist/brownfield/commands/packageIos.d.ts.map +1 -1
  5. package/dist/brownfield/commands/packageIos.js +4 -2
  6. package/dist/brownfield/commands/publishAndroid.d.ts.map +1 -1
  7. package/dist/brownfield/commands/publishAndroid.js +3 -1
  8. package/dist/brownie/commands/codegen.d.ts +4 -2
  9. package/dist/brownie/commands/codegen.d.ts.map +1 -1
  10. package/dist/brownie/commands/codegen.js +15 -7
  11. package/dist/brownie/config.d.ts +19 -5
  12. package/dist/brownie/config.d.ts.map +1 -1
  13. package/dist/brownie/config.js +45 -9
  14. package/dist/brownie/helpers/runBrownieCodegenIfApplicable.d.ts +1 -1
  15. package/dist/brownie/helpers/runBrownieCodegenIfApplicable.d.ts.map +1 -1
  16. package/dist/brownie/helpers/runBrownieCodegenIfApplicable.js +1 -1
  17. package/dist/brownie/index.d.ts +0 -1
  18. package/dist/brownie/index.d.ts.map +1 -1
  19. package/dist/config.d.ts +7 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +72 -0
  22. package/dist/navigation/commands/codegen.d.ts.map +1 -1
  23. package/dist/navigation/generators/ts.d.ts.map +1 -1
  24. package/dist/navigation/generators/ts.js +53 -4
  25. package/dist/navigation/runner.d.ts.map +1 -1
  26. package/dist/navigation/runner.js +16 -12
  27. package/dist/shared/utils/cli.d.ts +1 -1
  28. package/dist/shared/utils/cli.d.ts.map +1 -1
  29. package/dist/types.d.ts +30 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/package.json +9 -1
  32. package/src/brownfield/commands/packageAndroid.ts +5 -6
  33. package/src/brownfield/commands/packageIos.ts +18 -18
  34. package/src/brownfield/commands/publishAndroid.ts +4 -1
  35. package/src/brownie/commands/codegen.ts +35 -10
  36. package/src/brownie/config.ts +72 -15
  37. package/src/brownie/helpers/runBrownieCodegenIfApplicable.ts +2 -2
  38. package/src/brownie/index.ts +0 -1
  39. package/src/config.ts +119 -0
  40. package/src/navigation/commands/codegen.ts +1 -3
  41. package/src/navigation/generators/ts.ts +61 -3
  42. package/src/navigation/runner.ts +17 -12
  43. package/src/shared/utils/cli.ts +4 -2
  44. package/src/types.ts +51 -0
  45. package/dist/brownie/types.d.ts +0 -2
  46. package/dist/brownie/types.d.ts.map +0 -1
  47. package/src/brownie/types.ts +0 -1
  48. /package/dist/{brownie/types.js → types.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callstack/brownfield-cli",
3
- "version": "3.11.0",
3
+ "version": "3.13.0",
4
4
  "license": "MIT",
5
5
  "author": "Artur Morys-Magiera <artus9033@gmail.com>",
6
6
  "bin": {
@@ -37,6 +37,11 @@
37
37
  "types": "./dist/navigation/index.d.ts",
38
38
  "default": "./dist/navigation/index.js"
39
39
  },
40
+ "./types": {
41
+ "source": "./src/types.ts",
42
+ "types": "./dist/types.d.ts",
43
+ "default": "./dist/types.js"
44
+ },
40
45
  "./package.json": "./package.json"
41
46
  },
42
47
  "scripts": {
@@ -45,6 +50,7 @@
45
50
  "build:brownfield": "yarn run build",
46
51
  "build": "node -e \"const fs=require('fs'),p=require('path');const d=p.join('dist','index.d.ts');if(!fs.existsSync(d)){try{fs.unlinkSync('tsconfig.tsbuildinfo')}catch{}}\" && tsc -p tsconfig.json",
47
52
  "dev": "tsc -p tsconfig.json --watch",
53
+ "generate:schema": "ts-json-schema-generator --path src/types.ts --type BrownfieldConfig --out schema.json && prettier --write schema.json",
48
54
  "test": "vitest run"
49
55
  },
50
56
  "keywords": [
@@ -81,6 +87,7 @@
81
87
  "@rock-js/plugin-brownfield-android": "^0.13.3",
82
88
  "@rock-js/plugin-brownfield-ios": "^0.13.3",
83
89
  "@rock-js/tools": "^0.13.3",
90
+ "ajv": "^8.20.0",
84
91
  "commander": "^14.0.3",
85
92
  "quicktype-core": "^23.2.6",
86
93
  "quicktype-typescript-input": "^23.2.6",
@@ -100,6 +107,7 @@
100
107
  "eslint": "^9.39.3",
101
108
  "globals": "^17.3.0",
102
109
  "nodemon": "^3.1.14",
110
+ "ts-json-schema-generator": "^2.9.0",
103
111
  "typescript": "5.9.3",
104
112
  "vitest": "^4.1.4"
105
113
  },
@@ -15,16 +15,15 @@ import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
15
15
  import { getProjectInfo } from '../utils/project.js';
16
16
  import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
17
17
  import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
18
+ import { mergeBrownfieldConfigWithOptions } from '../../config.js';
18
19
 
19
20
  export const packageAndroidCommand = curryOptions(
20
21
  new Command('package:android').description('Build Android AAR'),
21
- packageAarOptions.map((option) =>
22
- option.name.startsWith('--variant')
23
- ? { ...option, default: 'debug' }
24
- : option
25
- )
22
+ packageAarOptions
26
23
  ).action(
27
- actionRunner(async (options: PackageAarFlags) => {
24
+ actionRunner(async (cliOptions: PackageAarFlags) => {
25
+ const options = mergeBrownfieldConfigWithOptions(cliOptions, 'android');
26
+
28
27
  const { projectRoot, platformConfig } = getProjectInfo('android');
29
28
  await runExpoPrebuildIfNeeded({
30
29
  projectRoot,
@@ -4,7 +4,6 @@ import path from 'node:path';
4
4
  import {
5
5
  getBuildOptions,
6
6
  mergeFrameworks,
7
- type BuildFlags as AppleBuildFlags,
8
7
  } from '@rock-js/platform-apple-helpers';
9
8
  import { packageIosAction } from '@rock-js/plugin-brownfield-ios';
10
9
  import {
@@ -30,7 +29,9 @@ import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNa
30
29
  import { copyDebugBundleToSimulatorSlice } from '../utils/copyDebugBundleToSimulatorSlice.js';
31
30
  import { resolvePackagedFrameworkName } from '../utils/resolvePackagedFrameworkName.js';
32
31
  import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
32
+ import type { PackageIosOptions } from '../../types.js';
33
33
  import { createLocalSpmPackage } from '../utils/createLocalSpmPackage.js';
34
+ import { mergeBrownfieldConfigWithOptions } from '../../config.js';
34
35
 
35
36
  /** Help text for `--use-prebuilt-rn-core` (keep in sync with docs/docs/docs/getting-started/ios.mdx, "React Native Prebuilts" section). */
36
37
  const USE_PREBUILT_RN_CORE_HELP =
@@ -69,13 +70,6 @@ function getPackagedFrameworkResolutionFailureMessage({
69
70
  : 'could not resolve the packaged framework output automatically; pass --scheme explicitly';
70
71
  }
71
72
 
72
- type PackageIosCliFlags = AppleBuildFlags & {
73
- /** Set when `--use-prebuilt-rn-core` is passed; omitted when the flag is absent (Rock applies RN version defaults). */
74
- usePrebuiltRnCore?: boolean;
75
- /** When set, generate a local Swift Package Manager manifest next to the packaged XCFramework outputs. */
76
- addSpmPackage?: boolean;
77
- };
78
-
79
73
  export const packageIosCommand = curryOptions(
80
74
  new Command('package:ios').description('Build iOS XCFramework'),
81
75
  getBuildOptions({ platformName: 'ios' }).map((option) =>
@@ -101,7 +95,9 @@ export const packageIosCommand = curryOptions(
101
95
  )
102
96
  )
103
97
  .action(
104
- actionRunner(async (options: PackageIosCliFlags) => {
98
+ actionRunner(async (cliOptions: PackageIosOptions) => {
99
+ const options = mergeBrownfieldConfigWithOptions(cliOptions, 'ios');
100
+
105
101
  const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios');
106
102
 
107
103
  const prebuiltRNCoreSupport = supportsPrebuiltRNCore({ projectRoot });
@@ -196,10 +192,12 @@ export const packageIosCommand = curryOptions(
196
192
 
197
193
  if (!frameworkName && options.addSpmPackage) {
198
194
  throw new RockError(
199
- `Cannot generate local SPM package: ${getPackagedFrameworkResolutionFailureMessage({
200
- resolution,
201
- candidates,
202
- })}`
195
+ `Cannot generate local SPM package: ${getPackagedFrameworkResolutionFailureMessage(
196
+ {
197
+ resolution,
198
+ candidates,
199
+ }
200
+ )}`
203
201
  );
204
202
  }
205
203
 
@@ -231,10 +229,12 @@ export const packageIosCommand = curryOptions(
231
229
  }
232
230
  } else if (configuration.includes('Debug')) {
233
231
  logger.warn(
234
- `Skipping Debug simulator JS bundle copy: ${getPackagedFrameworkResolutionFailureMessage({
235
- resolution,
236
- candidates,
237
- })}`
232
+ `Skipping Debug simulator JS bundle copy: ${getPackagedFrameworkResolutionFailureMessage(
233
+ {
234
+ resolution,
235
+ candidates,
236
+ }
237
+ )}`
238
238
  );
239
239
  }
240
240
 
@@ -326,7 +326,7 @@ export const packageIosCommand = curryOptions(
326
326
  `Add the local package folder in Xcode: ${colorLink(relativeToCwd(packageDir))}`
327
327
  );
328
328
  logger.info(
329
- "In Xcode, choose File > Add Package Dependencies..., click Add Local..., and select that folder."
329
+ 'In Xcode, choose File > Add Package Dependencies..., click Add Local..., and select that folder.'
330
330
  );
331
331
  }
332
332
  })
@@ -15,6 +15,7 @@ import { getProjectInfo } from '../utils/project.js';
15
15
  import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
16
16
  import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
17
17
  import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
18
+ import { mergeBrownfieldConfigWithOptions } from '../../config.js';
18
19
 
19
20
  export const publishAndroidCommand = curryOptions(
20
21
  new Command('publish:android').description(
@@ -22,7 +23,9 @@ export const publishAndroidCommand = curryOptions(
22
23
  ),
23
24
  publishLocalAarOptions
24
25
  ).action(
25
- actionRunner(async (options: PublishLocalAarFlags) => {
26
+ actionRunner(async (cliOptions: PublishLocalAarFlags) => {
27
+ const options = mergeBrownfieldConfigWithOptions(cliOptions, 'android');
28
+
26
29
  const { projectRoot, platformConfig } = getProjectInfo('android');
27
30
  await runExpoPrebuildIfNeeded({
28
31
  projectRoot,
@@ -7,15 +7,15 @@ import { intro, logger, outro } from '@rock-js/tools';
7
7
  import { QuickTypeError } from 'quicktype-core';
8
8
  import { actionRunner } from '../../shared/index.js';
9
9
  import {
10
- loadConfig,
11
10
  getSwiftOutputPath,
12
- type BrownieConfig,
11
+ resolveBrownieCodegenConfig,
13
12
  } from '../config.js';
13
+ import { findProjectRoot } from '../../brownfield/utils/paths.js';
14
14
  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
- import type { Platform } from '../types.js';
18
17
  import { NoBrownieStoresError } from '../errors/NoBrownieStoresError.js';
18
+ import type { BrownieConfig, Platform } from '../../types.js';
19
19
 
20
20
  function getOutputPath(dir: string, name: string, ext: string): string {
21
21
  return path.join(dir, `${name}.${ext}`);
@@ -34,7 +34,8 @@ async function generateForStore(
34
34
  store: DiscoveredStore,
35
35
  config: BrownieConfig,
36
36
  platforms: Platform[],
37
- showLabel: boolean
37
+ showLabel: boolean,
38
+ projectRoot: string
38
39
  ): Promise<void> {
39
40
  const { name, schemaPath } = store;
40
41
  const storeLabel = showLabel ? ` [${name}]` : '';
@@ -45,7 +46,7 @@ async function generateForStore(
45
46
  let outputPath: string;
46
47
 
47
48
  if (p === 'swift') {
48
- const swiftOutputDir = getSwiftOutputPath();
49
+ const swiftOutputDir = getSwiftOutputPath(projectRoot);
49
50
  outputPath = getOutputPath(swiftOutputDir, name, 'swift');
50
51
  } else {
51
52
  const kotlinOutputDir = config.kotlin;
@@ -84,17 +85,35 @@ async function generateForStore(
84
85
  }
85
86
  }
86
87
 
87
- export type RunCodegenOptions = { platform?: Platform };
88
+ export type RunCodegenOptions = {
89
+ platform?: Platform;
90
+ brownie?: BrownieConfig;
91
+ projectRoot?: string;
92
+ };
88
93
 
89
94
  /**
90
95
  * Runs the codegen command with the given arguments.
91
96
  */
92
- export async function runCodegen({ platform }: RunCodegenOptions) {
97
+ export async function runCodegen({
98
+ platform,
99
+ brownie,
100
+ projectRoot,
101
+ }: RunCodegenOptions) {
93
102
  intro(
94
103
  `Running Brownie codegen for ${platform ? `platform ${platform}` : 'all platforms'}`
95
104
  );
96
105
 
97
- const config = loadConfig();
106
+ const resolvedProjectRoot = projectRoot ?? findProjectRoot();
107
+ const { config, usedLegacyConfig } = resolveBrownieCodegenConfig({
108
+ brownie,
109
+ projectRoot: resolvedProjectRoot,
110
+ });
111
+
112
+ if (usedLegacyConfig) {
113
+ logger.warn(
114
+ 'You are using legacy Brownie configuration. Please migrate to the new configuration format. See the documentation for more details: https://oss.callstack.com/react-native-brownfield/docs/api-reference/configuration#migrating-from-legacy-brownie-configuration'
115
+ );
116
+ }
98
117
 
99
118
  if (platform && !['swift', 'kotlin'].includes(platform)) {
100
119
  logger.error(`Invalid platform: ${platform}. Must be 'swift' or 'kotlin'`);
@@ -102,7 +121,7 @@ export async function runCodegen({ platform }: RunCodegenOptions) {
102
121
  }
103
122
 
104
123
  try {
105
- const stores = discoverStores();
124
+ const stores = discoverStores(resolvedProjectRoot);
106
125
  const isMultipleStores = stores.length > 1;
107
126
  const schemaList = stores
108
127
  .map((s) => path.basename(s.schemaPath))
@@ -122,7 +141,13 @@ export async function runCodegen({ platform }: RunCodegenOptions) {
122
141
  platforms = ['swift', 'kotlin'];
123
142
  }
124
143
 
125
- await generateForStore(store, config, platforms, isMultipleStores);
144
+ await generateForStore(
145
+ store,
146
+ config,
147
+ platforms,
148
+ isMultipleStores,
149
+ resolvedProjectRoot
150
+ );
126
151
  }
127
152
  } catch (error) {
128
153
  if (error instanceof NoBrownieStoresError) {
@@ -1,21 +1,32 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
+ import { findProjectRoot } from '../brownfield/utils/paths.js';
5
+ import { loadBrownfieldConfig } from '../config.js';
6
+ import type { BrownieConfig } from '../types.js';
4
7
 
5
- export interface BrownieConfig {
6
- kotlin?: string;
7
- kotlinPackageName?: string;
8
- }
8
+ const LEGACY_AND_NEW_BROWNIE_CONFIG_ERROR =
9
+ 'Cannot use both legacy and new Brownie configuration formats simultaneously. Please migrate to the new configuration format and remove legacy configuration files: https://oss.callstack.com/react-native-brownfield/docs/api-reference/configuration#migrating-from-legacy-brownie-configuration';
9
10
 
10
11
  interface PackageJson {
11
12
  brownie?: BrownieConfig;
12
13
  }
13
14
 
15
+ function loadPackageJson(projectRoot: string = findProjectRoot()): PackageJson {
16
+ const packageJsonPath = path.resolve(projectRoot, 'package.json');
17
+
18
+ if (!fs.existsSync(packageJsonPath)) {
19
+ throw new Error('package.json not found');
20
+ }
21
+
22
+ return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as PackageJson;
23
+ }
24
+
14
25
  /**
15
26
  * Checks if @callstack/brownie package is installed.
16
27
  */
17
28
  export function isBrownieInstalled(
18
- projectRoot: string = process.cwd()
29
+ projectRoot: string = findProjectRoot()
19
30
  ): boolean {
20
31
  const require = createRequire(path.join(projectRoot, 'package.json'));
21
32
  try {
@@ -30,7 +41,7 @@ export function isBrownieInstalled(
30
41
  * Resolves the path to the @callstack/brownie package.
31
42
  */
32
43
  export function getBrowniePackagePath(
33
- projectRoot: string = process.cwd()
44
+ projectRoot: string = findProjectRoot()
34
45
  ): string {
35
46
  const require = createRequire(path.join(projectRoot, 'package.json'));
36
47
  try {
@@ -48,24 +59,70 @@ export function getBrowniePackagePath(
48
59
  * Returns the output path for generated Swift files.
49
60
  */
50
61
  export function getSwiftOutputPath(
51
- projectRoot: string = process.cwd()
62
+ projectRoot: string = findProjectRoot()
52
63
  ): string {
53
64
  const browniePath = getBrowniePackagePath(projectRoot);
54
65
  return path.join(browniePath, 'ios', 'Generated');
55
66
  }
56
67
 
68
+ /**
69
+ * Returns whether package.json contains legacy brownie config.
70
+ */
71
+ export function hasLegacyConfig(
72
+ projectRoot: string = findProjectRoot()
73
+ ): boolean {
74
+ const packageJson = loadPackageJson(projectRoot);
75
+
76
+ return Object.prototype.hasOwnProperty.call(packageJson, 'brownie');
77
+ }
78
+
57
79
  /**
58
80
  * Loads brownie config from package.json in the current working directory.
59
81
  */
60
- export function loadConfig(): BrownieConfig {
61
- const packageJsonPath = path.resolve(process.cwd(), 'package.json');
82
+ export function loadConfig(
83
+ projectRoot: string = findProjectRoot()
84
+ ): BrownieConfig {
85
+ const packageJson = loadPackageJson(projectRoot);
62
86
 
63
- if (!fs.existsSync(packageJsonPath)) {
64
- throw new Error('package.json not found');
87
+ return packageJson.brownie ?? {};
88
+ }
89
+
90
+ export type ResolveBrownieCodegenConfigOptions = {
91
+ brownie?: BrownieConfig;
92
+ projectRoot?: string;
93
+ };
94
+
95
+ export type ResolvedBrownieCodegenConfig = {
96
+ config: BrownieConfig;
97
+ usedLegacyConfig: boolean;
98
+ };
99
+
100
+ /**
101
+ * Resolves Brownie codegen settings from explicit options, brownfield config,
102
+ * or legacy package.json `brownie` config.
103
+ */
104
+ export function resolveBrownieCodegenConfig({
105
+ brownie,
106
+ projectRoot = findProjectRoot(),
107
+ }: ResolveBrownieCodegenConfigOptions = {}): ResolvedBrownieCodegenConfig {
108
+ const legacyConfig = hasLegacyConfig(projectRoot)
109
+ ? loadConfig(projectRoot)
110
+ : undefined;
111
+ const brownfieldBrownie = loadBrownfieldConfig(projectRoot).brownie;
112
+
113
+ if (legacyConfig !== undefined && (brownie ?? brownfieldBrownie)) {
114
+ throw new Error(LEGACY_AND_NEW_BROWNIE_CONFIG_ERROR);
65
115
  }
66
116
 
67
- const packageJson: PackageJson = JSON.parse(
68
- fs.readFileSync(packageJsonPath, 'utf-8')
69
- );
70
- return packageJson.brownie ?? {};
117
+ if (legacyConfig !== undefined) {
118
+ return {
119
+ config: legacyConfig,
120
+ usedLegacyConfig: true,
121
+ };
122
+ }
123
+
124
+ return {
125
+ config: brownie ?? brownfieldBrownie ?? {},
126
+ usedLegacyConfig: false,
127
+ };
71
128
  }
@@ -1,7 +1,7 @@
1
1
  import { runCodegen } from '../commands/codegen.js';
2
2
  import { isBrownieInstalled } from '../config.js';
3
3
 
4
- import type { Platform } from '../types.js';
4
+ import type { Platform } from '../../types.js';
5
5
 
6
6
  export async function runBrownieCodegenIfApplicable(
7
7
  projectRoot: string,
@@ -9,7 +9,7 @@ export async function runBrownieCodegenIfApplicable(
9
9
  ) {
10
10
  const hasBrownie = isBrownieInstalled(projectRoot);
11
11
  if (hasBrownie) {
12
- await runCodegen({ platform });
12
+ await runCodegen({ platform, projectRoot });
13
13
  }
14
14
 
15
15
  return { hasBrownie };
@@ -2,7 +2,6 @@ import { styleText } from 'node:util';
2
2
 
3
3
  import * as Commands from './commands/index.js';
4
4
 
5
- export type * from './types.js';
6
5
  export * from './store-discovery.js';
7
6
 
8
7
  export const groupName = `${styleText(['bold', 'blueBright'], '@callstack/brownie')}${styleText('whiteBright', ' - Shared state management CLI for React Native Brownfield')}`;
package/src/config.ts ADDED
@@ -0,0 +1,119 @@
1
+ import fs from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+
5
+ import Ajv from 'ajv';
6
+
7
+ import type { BrownfieldConfig } from './types.js';
8
+ import { findProjectRoot } from './brownfield/utils/paths.js';
9
+
10
+ import BrownfieldSchema from '../schema.json' with { type: 'json' };
11
+ import { logger } from '@rock-js/tools';
12
+
13
+ const CONFIG_BASE_NAME = 'brownfield';
14
+ const JS_CONFIG_FILE_NAME = `${CONFIG_BASE_NAME}.config.js`;
15
+ const JSON_CONFIG_FILE_NAME = `${CONFIG_BASE_NAME}.config.json`;
16
+
17
+ const SEPARATOR = '\n● ';
18
+
19
+ const ajv = new Ajv({ allErrors: true });
20
+ const validateBrownfieldConfig = ajv.compile(BrownfieldSchema);
21
+
22
+ export function validateBrownfieldCLIConfig(config: unknown): void {
23
+ if (!validateBrownfieldConfig(config)) {
24
+ logger.warn(
25
+ `Brownfield configuration has some issues: ${SEPARATOR}${ajv.errorsText(validateBrownfieldConfig.errors, { separator: SEPARATOR, dataVar: 'config' })}.`
26
+ );
27
+ }
28
+ }
29
+
30
+ export function loadBrownfieldConfig(
31
+ projectRoot: string = findProjectRoot()
32
+ ): BrownfieldConfig {
33
+ const require = createRequire(path.join(projectRoot, 'package.json'));
34
+
35
+ const jsConfigFilePath = path.join(projectRoot, JS_CONFIG_FILE_NAME);
36
+ const jsonConfigFilePath = path.join(projectRoot, JSON_CONFIG_FILE_NAME);
37
+ const packageJsonPath = path.join(projectRoot, 'package.json');
38
+ const packageJson = require(packageJsonPath) as Record<string, unknown>;
39
+
40
+ if (
41
+ [
42
+ fs.existsSync(jsConfigFilePath),
43
+ fs.existsSync(jsonConfigFilePath),
44
+ packageJson[CONFIG_BASE_NAME],
45
+ ].filter(Boolean).length > 1
46
+ ) {
47
+ throw new Error('Project has multiple Brownfield configuration files');
48
+ }
49
+
50
+ if (fs.existsSync(jsConfigFilePath)) {
51
+ return require(jsConfigFilePath) as BrownfieldConfig;
52
+ }
53
+
54
+ if (fs.existsSync(jsonConfigFilePath)) {
55
+ return require(jsonConfigFilePath) as BrownfieldConfig;
56
+ }
57
+
58
+ return packageJson[CONFIG_BASE_NAME] || {};
59
+ }
60
+
61
+ type BrownfieldPlatform = 'android' | 'ios';
62
+ type ConfigurableOptions = Record<string, unknown>;
63
+
64
+ function getSharedConfig(config: BrownfieldConfig): ConfigurableOptions {
65
+ return config.verbose === undefined ? {} : { verbose: config.verbose };
66
+ }
67
+
68
+ function formatConfigValue(value: unknown): string {
69
+ return typeof value === 'string' ? value : JSON.stringify(value);
70
+ }
71
+
72
+ function areConfigValuesEqual(configValue: unknown, optionValue: unknown) {
73
+ return JSON.stringify(configValue) === JSON.stringify(optionValue);
74
+ }
75
+
76
+ export function mergeBrownfieldConfigWithOptions<T extends object>(
77
+ options: T,
78
+ platform: BrownfieldPlatform
79
+ ): T {
80
+ const reactNativeBrownfieldConfig = loadBrownfieldConfig();
81
+
82
+ validateBrownfieldCLIConfig(reactNativeBrownfieldConfig);
83
+
84
+ const platformConfig: ConfigurableOptions = {
85
+ ...getSharedConfig(reactNativeBrownfieldConfig),
86
+ ...reactNativeBrownfieldConfig[platform],
87
+ };
88
+
89
+ const cliOptions = Object.fromEntries(
90
+ Object.entries(options).filter(([, value]) => value !== undefined)
91
+ );
92
+
93
+ for (const [key, value] of Object.entries(cliOptions)) {
94
+ const configValue = platformConfig[key];
95
+
96
+ if (
97
+ configValue !== undefined &&
98
+ !areConfigValuesEqual(configValue, value)
99
+ ) {
100
+ logger.warn(
101
+ 'CLI option "%s" is overriding the react-native-brownfield config value: %s -> %s.',
102
+ key,
103
+ formatConfigValue(configValue),
104
+ formatConfigValue(value)
105
+ );
106
+ }
107
+ }
108
+
109
+ const mergedOptions = {
110
+ ...platformConfig,
111
+ ...cliOptions,
112
+ } as T & { verbose?: unknown };
113
+
114
+ if (typeof mergedOptions.verbose === 'boolean') {
115
+ logger.setVerbose(mergedOptions.verbose);
116
+ }
117
+
118
+ return mergedOptions;
119
+ }
@@ -42,9 +42,7 @@ export const navigationCodegenCommand = new Command('navigation:codegen')
42
42
  const specPath = typeof args[0] === 'string' ? args[0] : undefined;
43
43
  const options =
44
44
  args.find(
45
- (
46
- arg
47
- ): arg is RunNavigationCodegenCommandOptions =>
45
+ (arg): arg is RunNavigationCodegenCommandOptions =>
48
46
  typeof arg === 'object' && arg !== null && 'dryRun' in arg
49
47
  ) ?? {};
50
48
 
@@ -56,14 +56,72 @@ export default TurboModuleRegistry.getEnforcing<Spec>(
56
56
  `;
57
57
  }
58
58
 
59
+ const SKIP_TYPE_TOKENS = new Set([
60
+ 'Array',
61
+ 'Date',
62
+ 'Map',
63
+ 'Object',
64
+ 'Promise',
65
+ 'ReadonlyArray',
66
+ 'Record',
67
+ 'Set',
68
+ 'any',
69
+ 'boolean',
70
+ 'false',
71
+ 'null',
72
+ 'number',
73
+ 'object',
74
+ 'string',
75
+ 'true',
76
+ 'undefined',
77
+ 'unknown',
78
+ 'void',
79
+ ]);
80
+
81
+ function collectTypesUsedByMethods(methods: MethodSignature[]): Set<string> {
82
+ const used = new Set<string>();
83
+
84
+ for (const method of methods) {
85
+ const typeTexts = [
86
+ method.returnType,
87
+ ...method.params.map((param) => param.type),
88
+ ];
89
+
90
+ for (const typeText of typeTexts) {
91
+ const matches = typeText.match(/\b[A-Za-z_]\w*\b/g);
92
+ if (!matches) {
93
+ continue;
94
+ }
95
+
96
+ for (const name of matches) {
97
+ if (!SKIP_TYPE_TOKENS.has(name)) {
98
+ used.add(name);
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ return used;
105
+ }
106
+
59
107
  function buildTypeImportLine(
108
+ methods: MethodSignature[],
60
109
  referencedTypeDeclarations: TypeDeclaration[]
61
110
  ): string {
62
111
  if (referencedTypeDeclarations.length === 0) {
63
112
  return '';
64
113
  }
65
114
 
66
- const names = referencedTypeDeclarations.map((entry) => entry.name).join(', ');
115
+ const usedTypes = collectTypesUsedByMethods(methods);
116
+ const names = referencedTypeDeclarations
117
+ .map((entry) => entry.name)
118
+ .filter((name) => usedTypes.has(name))
119
+ .join(', ');
120
+
121
+ if (names.length === 0) {
122
+ return '';
123
+ }
124
+
67
125
  return `import type { ${names} } from './NativeBrownfieldNavigation';\n`;
68
126
  }
69
127
 
@@ -71,7 +129,7 @@ export function generateIndexTs(
71
129
  methods: MethodSignature[],
72
130
  referencedTypeDeclarations: TypeDeclaration[] = []
73
131
  ): string {
74
- const typeImportLine = buildTypeImportLine(referencedTypeDeclarations);
132
+ const typeImportLine = buildTypeImportLine(methods, referencedTypeDeclarations);
75
133
  const functionImplementations = methods
76
134
  .map((method) => {
77
135
  const params = method.params
@@ -155,7 +213,7 @@ export function generateIndexDts(
155
213
  methods: MethodSignature[],
156
214
  referencedTypeDeclarations: TypeDeclaration[] = []
157
215
  ): string {
158
- const typeImportLine = buildTypeImportLine(referencedTypeDeclarations);
216
+ const typeImportLine = buildTypeImportLine(methods, referencedTypeDeclarations);
159
217
  const methodSignatures = methods
160
218
  .map((method) => {
161
219
  const params = method.params