Skip to content

Commit c651898

Browse files
committed
feat: handle multiple stores
1 parent cee62ee commit c651898

6 files changed

Lines changed: 130 additions & 77 deletions

File tree

apps/example/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
!.yarn/versions
77

88
swift/Generated/**
9+
kotlin/**/Generated/**

apps/example/kotlin/app/src/main/java/com/callstack/kotlinexample/BrownfieldStore.kt

Lines changed: 0 additions & 8 deletions
This file was deleted.

apps/example/package.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@
3434
"node": ">=20"
3535
},
3636
"brownie": {
37-
"stores": {
38-
"schema": "./brownfield-store.schema.ts",
39-
"typeName": "BrownfieldStore",
40-
"swift": "./swift/Generated/BrownfieldStore.swift",
41-
"kotlin": "./kotlin/app/src/main/java/com/callstack/kotlinexample/BrownfieldStore.kt",
42-
"kotlinPackageName": "com.callstack.kotlinexample"
43-
}
37+
"stores": [
38+
{
39+
"schema": "./brownfield-store.schema.ts",
40+
"typeName": "BrownfieldStore",
41+
"swift": "./swift/Generated/BrownfieldStore.swift",
42+
"kotlin": "./kotlin/app/src/main/java/com/callstack/kotlinexample/Generated/BrownfieldStore.kt",
43+
"kotlinPackageName": "com.callstack.kotlinexample"
44+
}
45+
]
4446
}
4547
}

brownfield-store-plan.md

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,40 @@ Add to your app's `package.json`:
5252
```json
5353
{
5454
"brownie": {
55-
"stores": {
56-
"schema": "./brownfield-store.schema.ts",
57-
"typeName": "BrownfieldStore",
58-
"swift": "./ios/Generated/BrownfieldStore.swift",
59-
"kotlin": "./android/app/src/main/java/com/example/BrownfieldStore.kt",
60-
"kotlinPackageName": "com.example"
61-
}
55+
"stores": [
56+
{
57+
"schema": "./brownfield-store.schema.ts",
58+
"typeName": "BrownfieldStore",
59+
"swift": "./ios/Generated/BrownfieldStore.swift",
60+
"kotlin": "./android/app/src/main/java/com/example/BrownfieldStore.kt",
61+
"kotlinPackageName": "com.example"
62+
}
63+
]
64+
}
65+
}
66+
```
67+
68+
Multiple stores example:
69+
70+
```json
71+
{
72+
"brownie": {
73+
"stores": [
74+
{
75+
"schema": "./stores/user.schema.ts",
76+
"typeName": "UserStore",
77+
"swift": "./ios/Generated/UserStore.swift",
78+
"kotlin": "./android/app/src/main/java/com/example/UserStore.kt",
79+
"kotlinPackageName": "com.example"
80+
},
81+
{
82+
"schema": "./stores/settings.schema.ts",
83+
"typeName": "SettingsStore",
84+
"swift": "./ios/Generated/SettingsStore.swift",
85+
"kotlin": "./android/app/src/main/java/com/example/SettingsStore.kt",
86+
"kotlinPackageName": "com.example"
87+
}
88+
]
6289
}
6390
}
6491
```
@@ -234,7 +261,7 @@ preBuild.dependsOn("generateBrownfieldStore")
234261

235262
### Codegen
236263

237-
- [ ] Support multiple stores in package.json config (array of store configs)
264+
- [x] Support multiple stores in package.json config (array of store configs)
238265
- [ ] Generate store keys enum/constants for type safety between JS and Native
239266

240267
### Native Runtime

packages/brownie/scripts/commands/codegen.ts

Lines changed: 58 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,51 @@
11
import { parseArgs, styleText } from 'node:util';
2-
import { loadConfig } from '../config';
2+
import { loadConfig, type StoresConfig } from '../config';
33
import { generateSwift } from '../generators/swift';
44
import { generateKotlin } from '../generators/kotlin';
55

66
type Platform = 'swift' | 'kotlin';
77

8+
async function generateForStore(
9+
config: StoresConfig,
10+
platforms: Platform[],
11+
storeIndex?: number
12+
): Promise<void> {
13+
const storeLabel = storeIndex !== undefined ? ` [${config.typeName}]` : '';
14+
15+
for (const p of platforms) {
16+
const outputPath = config[p];
17+
if (!outputPath) {
18+
continue;
19+
}
20+
21+
try {
22+
if (p === 'swift') {
23+
await generateSwift({
24+
schemaPath: config.schema,
25+
typeName: config.typeName,
26+
outputPath,
27+
});
28+
} else {
29+
await generateKotlin({
30+
schemaPath: config.schema,
31+
typeName: config.typeName,
32+
outputPath,
33+
packageName: config.kotlinPackageName,
34+
});
35+
}
36+
console.log(styleText('green', ` ✓ ${outputPath}${storeLabel}`));
37+
} catch (error) {
38+
console.error(
39+
styleText(
40+
'red',
41+
`Error generating ${p}${storeLabel}: ${error instanceof Error ? error.message : error}`
42+
)
43+
);
44+
process.exit(1);
45+
}
46+
}
47+
}
48+
849
const HELP_TEXT = `
950
${styleText('bold', 'brownie codegen')} - Generate native store types from TypeScript schema
1051
@@ -50,7 +91,7 @@ export async function runCodegen(
5091
return;
5192
}
5293

53-
const config = loadConfig();
94+
const configs = loadConfig();
5495
const platform = values.platform as Platform | undefined;
5596

5697
if (platform && !['swift', 'kotlin'].includes(platform)) {
@@ -63,55 +104,29 @@ export async function runCodegen(
63104
process.exit(1);
64105
}
65106

66-
const platforms: Platform[] = platform
67-
? [platform]
68-
: (['swift', 'kotlin'] as Platform[]).filter((p) => config[p]);
69-
70-
if (platforms.length === 0) {
71-
console.error(
72-
styleText('red', 'No output paths configured in brownie.stores')
73-
);
74-
process.exit(1);
75-
}
76-
107+
const isMultipleStores = configs.length > 1;
108+
const schemaList = configs.map((c) => c.schema).join(', ');
77109
console.log(
78-
styleText('cyan', `Generating store types from ${config.schema}...`)
110+
styleText('cyan', `Generating store types from ${schemaList}...`)
79111
);
80112

81-
for (const p of platforms) {
82-
const outputPath = config[p];
83-
if (!outputPath) {
84-
console.warn(
85-
styleText('yellow', `Skipping ${p}: no output path configured`)
86-
);
87-
continue;
88-
}
113+
for (let i = 0; i < configs.length; i++) {
114+
const config = configs[i];
115+
const platforms: Platform[] = platform
116+
? [platform]
117+
: (['swift', 'kotlin'] as Platform[]).filter((p) => config[p]);
89118

90-
try {
91-
if (p === 'swift') {
92-
await generateSwift({
93-
schemaPath: config.schema,
94-
typeName: config.typeName,
95-
outputPath,
96-
});
97-
} else {
98-
await generateKotlin({
99-
schemaPath: config.schema,
100-
typeName: config.typeName,
101-
outputPath,
102-
packageName: config.kotlinPackageName,
103-
});
104-
}
105-
console.log(styleText('green', ` ✓ ${outputPath}`));
106-
} catch (error) {
107-
console.error(
119+
if (platforms.length === 0) {
120+
console.warn(
108121
styleText(
109-
'red',
110-
`Error generating ${p}: ${error instanceof Error ? error.message : error}`
122+
'yellow',
123+
`No output paths configured for store ${config.typeName}`
111124
)
112125
);
113-
process.exit(1);
126+
continue;
114127
}
128+
129+
await generateForStore(config, platforms, isMultipleStores ? i : undefined);
115130
}
116131

117132
console.log(styleText('green', '\nDone!'));

packages/brownie/scripts/config.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,37 @@ export interface StoresConfig {
1010
}
1111

1212
export interface BrownieConfig {
13-
stores?: StoresConfig;
13+
stores?: StoresConfig[];
1414
}
1515

1616
interface PackageJson {
1717
brownie?: BrownieConfig;
1818
}
1919

20+
function validateStoreConfig(config: StoresConfig, index?: number): void {
21+
const prefix =
22+
index !== undefined ? `brownie.stores[${index}]` : 'brownie.stores';
23+
24+
if (!config.schema) {
25+
throw new Error(`${prefix}.schema is required`);
26+
}
27+
28+
if (!config.typeName) {
29+
throw new Error(`${prefix}.typeName is required`);
30+
}
31+
32+
if (!config.swift && !config.kotlin) {
33+
throw new Error(
34+
`At least one output path is required: ${prefix}.swift or ${prefix}.kotlin`
35+
);
36+
}
37+
}
38+
2039
/**
2140
* Loads brownie config from package.json in the current working directory.
41+
* Supports both single store config and array of store configs.
2242
*/
23-
export function loadConfig(): StoresConfig {
43+
export function loadConfig(): StoresConfig[] {
2444
const packageJsonPath = path.resolve(process.cwd(), 'package.json');
2545

2646
if (!fs.existsSync(packageJsonPath)) {
@@ -36,19 +56,15 @@ export function loadConfig(): StoresConfig {
3656
throw new Error('brownie.stores config not found in package.json');
3757
}
3858

39-
if (!config.schema) {
40-
throw new Error('brownie.stores.schema is required');
59+
if (!Array.isArray(config)) {
60+
throw new Error('brownie.stores must be an array');
4161
}
4262

43-
if (!config.typeName) {
44-
throw new Error('brownie.stores.typeName is required');
63+
if (config.length === 0) {
64+
throw new Error('brownie.stores array cannot be empty');
4565
}
4666

47-
if (!config.swift && !config.kotlin) {
48-
throw new Error(
49-
'At least one output path is required: brownie.stores.swift or brownie.stores.kotlin'
50-
);
51-
}
67+
config.forEach((c, i) => validateStoreConfig(c, i));
5268

5369
return config;
5470
}

0 commit comments

Comments
 (0)