@callstack/brownfield-cli 3.12.0 → 3.13.1

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 (49) hide show
  1. package/CHANGELOG.md +12 -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 +11 -2
  32. package/schema.json +110 -0
  33. package/src/brownfield/commands/packageAndroid.ts +5 -6
  34. package/src/brownfield/commands/packageIos.ts +18 -18
  35. package/src/brownfield/commands/publishAndroid.ts +4 -1
  36. package/src/brownie/commands/codegen.ts +35 -10
  37. package/src/brownie/config.ts +72 -15
  38. package/src/brownie/helpers/runBrownieCodegenIfApplicable.ts +2 -2
  39. package/src/brownie/index.ts +0 -1
  40. package/src/config.ts +119 -0
  41. package/src/navigation/commands/codegen.ts +1 -3
  42. package/src/navigation/generators/ts.ts +61 -3
  43. package/src/navigation/runner.ts +17 -12
  44. package/src/shared/utils/cli.ts +4 -2
  45. package/src/types.ts +51 -0
  46. package/dist/brownie/types.d.ts +0 -2
  47. package/dist/brownie/types.d.ts.map +0 -1
  48. package/src/brownie/types.ts +0 -1
  49. /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.12.0",
3
+ "version": "3.13.1",
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": [
@@ -63,7 +69,8 @@
63
69
  "!**/__fixtures__",
64
70
  "!**/__mocks__",
65
71
  "!**/.*",
66
- "README.md"
72
+ "README.md",
73
+ "schema.json"
67
74
  ],
68
75
  "publishConfig": {
69
76
  "access": "public"
@@ -81,6 +88,7 @@
81
88
  "@rock-js/plugin-brownfield-android": "^0.13.3",
82
89
  "@rock-js/plugin-brownfield-ios": "^0.13.3",
83
90
  "@rock-js/tools": "^0.13.3",
91
+ "ajv": "^8.20.0",
84
92
  "commander": "^14.0.3",
85
93
  "quicktype-core": "^23.2.6",
86
94
  "quicktype-typescript-input": "^23.2.6",
@@ -100,6 +108,7 @@
100
108
  "eslint": "^9.39.3",
101
109
  "globals": "^17.3.0",
102
110
  "nodemon": "^3.1.14",
111
+ "ts-json-schema-generator": "^2.9.0",
103
112
  "typescript": "5.9.3",
104
113
  "vitest": "^4.1.4"
105
114
  },
package/schema.json ADDED
@@ -0,0 +1,110 @@
1
+ {
2
+ "$ref": "#/definitions/BrownfieldConfig",
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "definitions": {
5
+ "BrownfieldAndroidConfig": {
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "moduleName": {
9
+ "type": "string"
10
+ },
11
+ "variant": {
12
+ "type": "string"
13
+ }
14
+ },
15
+ "type": "object"
16
+ },
17
+ "BrownfieldConfig": {
18
+ "additionalProperties": false,
19
+ "properties": {
20
+ "$schema": {
21
+ "type": "string"
22
+ },
23
+ "android": {
24
+ "$ref": "#/definitions/BrownfieldAndroidConfig"
25
+ },
26
+ "brownie": {
27
+ "$ref": "#/definitions/BrownieConfig"
28
+ },
29
+ "ios": {
30
+ "$ref": "#/definitions/BrownfieldIosConfig"
31
+ },
32
+ "verbose": {
33
+ "type": "boolean"
34
+ }
35
+ },
36
+ "type": "object"
37
+ },
38
+ "BrownfieldIosConfig": {
39
+ "additionalProperties": false,
40
+ "properties": {
41
+ "addSpmPackage": {
42
+ "description": "When set, generate a local Swift Package Manager manifest next to the packaged XCFramework outputs.",
43
+ "type": "boolean"
44
+ },
45
+ "archive": {
46
+ "type": "boolean"
47
+ },
48
+ "buildFolder": {
49
+ "type": "string"
50
+ },
51
+ "configuration": {
52
+ "type": "string"
53
+ },
54
+ "destination": {
55
+ "items": {
56
+ "type": "string"
57
+ },
58
+ "type": "array"
59
+ },
60
+ "exportExtraParams": {
61
+ "items": {
62
+ "type": "string"
63
+ },
64
+ "type": "array"
65
+ },
66
+ "exportOptionsPlist": {
67
+ "type": "string"
68
+ },
69
+ "extraParams": {
70
+ "items": {
71
+ "type": "string"
72
+ },
73
+ "type": "array"
74
+ },
75
+ "installPods": {
76
+ "type": "boolean"
77
+ },
78
+ "local": {
79
+ "type": "boolean"
80
+ },
81
+ "newArch": {
82
+ "type": "boolean"
83
+ },
84
+ "scheme": {
85
+ "type": "string"
86
+ },
87
+ "target": {
88
+ "type": "string"
89
+ },
90
+ "usePrebuiltRnCore": {
91
+ "description": "Set when `--use-prebuilt-rn-core` is passed; omitted when the flag is absent (Rock applies RN version defaults).",
92
+ "type": "boolean"
93
+ }
94
+ },
95
+ "type": "object"
96
+ },
97
+ "BrownieConfig": {
98
+ "additionalProperties": false,
99
+ "properties": {
100
+ "kotlin": {
101
+ "type": "string"
102
+ },
103
+ "kotlinPackageName": {
104
+ "type": "string"
105
+ }
106
+ },
107
+ "type": "object"
108
+ }
109
+ }
110
+ }
@@ -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