@callstack/brownfield-cli 1.0.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.
- package/dist/brownfield/commands/index.d.ts +4 -0
- package/dist/brownfield/commands/index.d.ts.map +1 -0
- package/dist/brownfield/commands/index.js +3 -0
- package/dist/brownfield/commands/packageAndroid.d.ts +5 -0
- package/dist/brownfield/commands/packageAndroid.d.ts.map +1 -0
- package/dist/brownfield/commands/packageAndroid.js +17 -0
- package/dist/brownfield/commands/packageIos.d.ts +5 -0
- package/dist/brownfield/commands/packageIos.d.ts.map +1 -0
- package/dist/brownfield/commands/packageIos.js +57 -0
- package/dist/brownfield/commands/publishAndroid.d.ts +5 -0
- package/dist/brownfield/commands/publishAndroid.d.ts.map +1 -0
- package/dist/brownfield/commands/publishAndroid.js +14 -0
- package/dist/brownfield/index.d.ts +12 -0
- package/dist/brownfield/index.d.ts.map +1 -0
- package/dist/brownfield/index.js +15 -0
- package/dist/brownfield/types/actionRunner.test.d.ts +2 -0
- package/dist/brownfield/types/actionRunner.test.d.ts.map +1 -0
- package/dist/brownfield/types/actionRunner.test.js +1 -0
- package/dist/brownfield/utils/index.d.ts +3 -0
- package/dist/brownfield/utils/index.d.ts.map +1 -0
- package/dist/brownfield/utils/index.js +2 -0
- package/dist/brownfield/utils/paths.d.ts +8 -0
- package/dist/brownfield/utils/paths.d.ts.map +1 -0
- package/dist/brownfield/utils/paths.js +24 -0
- package/dist/brownfield/utils/rn-cli.d.ts +17 -0
- package/dist/brownfield/utils/rn-cli.d.ts.map +1 -0
- package/dist/brownfield/utils/rn-cli.js +31 -0
- package/dist/brownie/commands/codegen.d.ts +11 -0
- package/dist/brownie/commands/codegen.d.ts.map +1 -0
- package/dist/brownie/commands/codegen.js +96 -0
- package/dist/brownie/commands/index.d.ts +2 -0
- package/dist/brownie/commands/index.d.ts.map +1 -0
- package/dist/brownie/commands/index.js +1 -0
- package/dist/brownie/config.d.ts +21 -0
- package/dist/brownie/config.d.ts.map +1 -0
- package/dist/brownie/config.js +47 -0
- package/dist/brownie/generators/kotlin.d.ts +12 -0
- package/dist/brownie/generators/kotlin.d.ts.map +1 -0
- package/dist/brownie/generators/kotlin.js +69 -0
- package/dist/brownie/generators/swift.d.ts +11 -0
- package/dist/brownie/generators/swift.d.ts.map +1 -0
- package/dist/brownie/generators/swift.js +55 -0
- package/dist/brownie/index.d.ts +7 -0
- package/dist/brownie/index.d.ts.map +1 -0
- package/dist/brownie/index.js +6 -0
- package/dist/brownie/store-discovery.d.ts +10 -0
- package/dist/brownie/store-discovery.d.ts.map +1 -0
- package/dist/brownie/store-discovery.js +75 -0
- package/dist/brownie/types.d.ts +2 -0
- package/dist/brownie/types.d.ts.map +1 -0
- package/dist/brownie/types.js +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +54 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +3 -0
- package/dist/shared/classes/ExampleUsage.d.ts +6 -0
- package/dist/shared/classes/ExampleUsage.d.ts.map +1 -0
- package/dist/shared/classes/ExampleUsage.js +8 -0
- package/dist/shared/classes/index.d.ts +2 -0
- package/dist/shared/classes/index.d.ts.map +1 -0
- package/dist/shared/classes/index.js +1 -0
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +2 -0
- package/dist/shared/utils/cli.d.ts +5 -0
- package/dist/shared/utils/cli.d.ts.map +1 -0
- package/dist/shared/utils/cli.js +36 -0
- package/dist/shared/utils/index.d.ts +2 -0
- package/dist/shared/utils/index.d.ts.map +1 -0
- package/dist/shared/utils/index.js +1 -0
- package/package.json +98 -0
- package/src/brownfield/commands/packageAndroid.ts +39 -0
- package/src/brownfield/commands/packageIos.ts +114 -0
- package/src/brownfield/commands/publishAndroid.ts +36 -0
- package/src/brownfield/index.ts +24 -0
- package/src/brownfield/utils/index.ts +2 -0
- package/src/brownfield/utils/paths.ts +40 -0
- package/src/brownfield/utils/rn-cli.ts +58 -0
- package/src/brownie/commands/codegen.ts +139 -0
- package/src/brownie/commands/index.ts +1 -0
- package/src/brownie/config.ts +71 -0
- package/src/brownie/generators/kotlin.ts +113 -0
- package/src/brownie/generators/swift.ts +87 -0
- package/src/brownie/index.ts +11 -0
- package/src/brownie/store-discovery.ts +108 -0
- package/src/brownie/types.ts +1 -0
- package/src/index.ts +84 -0
- package/src/main.ts +5 -0
- package/src/shared/classes/ExampleUsage.ts +6 -0
- package/src/shared/classes/index.ts +1 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/utils/cli.ts +47 -0
- package/src/shared/utils/index.ts +1 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getBuildOptions,
|
|
5
|
+
mergeFrameworks,
|
|
6
|
+
type BuildFlags as AppleBuildFlags,
|
|
7
|
+
} from '@rock-js/platform-apple-helpers';
|
|
8
|
+
import { packageIosAction } from '@rock-js/plugin-brownfield-ios';
|
|
9
|
+
import {
|
|
10
|
+
colorLink,
|
|
11
|
+
getReactNativeVersion,
|
|
12
|
+
logger,
|
|
13
|
+
relativeToCwd,
|
|
14
|
+
} from '@rock-js/tools';
|
|
15
|
+
|
|
16
|
+
import { Command } from 'commander';
|
|
17
|
+
|
|
18
|
+
import { isBrownieInstalled } from '../../brownie/config.js';
|
|
19
|
+
import { runCodegen } from '../../brownie/commands/codegen.js';
|
|
20
|
+
import { getProjectInfo } from '../utils/index.js';
|
|
21
|
+
import {
|
|
22
|
+
actionRunner,
|
|
23
|
+
curryOptions,
|
|
24
|
+
ExampleUsage,
|
|
25
|
+
} from '../../shared/index.js';
|
|
26
|
+
|
|
27
|
+
export const packageIosCommand = curryOptions(
|
|
28
|
+
new Command('package:ios').description('Build iOS XCFramework'),
|
|
29
|
+
getBuildOptions({ platformName: 'ios' }).map((option) =>
|
|
30
|
+
option.name.startsWith('--build-folder')
|
|
31
|
+
? {
|
|
32
|
+
...option,
|
|
33
|
+
description:
|
|
34
|
+
option.description +
|
|
35
|
+
" By default, the '<iOS project folder>/build' path will be used.",
|
|
36
|
+
}
|
|
37
|
+
: option
|
|
38
|
+
)
|
|
39
|
+
).action(
|
|
40
|
+
actionRunner(async (options: AppleBuildFlags) => {
|
|
41
|
+
const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios');
|
|
42
|
+
|
|
43
|
+
if (!userConfig.project.ios) {
|
|
44
|
+
throw new Error('iOS project not found.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!userConfig.project.ios.xcodeProject) {
|
|
48
|
+
throw new Error('iOS Xcode project not found in the configuration.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const brownieCacheDir = path.join(
|
|
52
|
+
userConfig.project.ios.sourceDir,
|
|
53
|
+
'.brownfield'
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
options.buildFolder ??= path.join(brownieCacheDir, 'build');
|
|
57
|
+
const packageDir = path.join(brownieCacheDir, 'package');
|
|
58
|
+
const configuration = options.configuration ?? 'Debug';
|
|
59
|
+
|
|
60
|
+
const hasBrownie = isBrownieInstalled(projectRoot);
|
|
61
|
+
if (hasBrownie) {
|
|
62
|
+
await runCodegen({ platform: 'swift' });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await packageIosAction(
|
|
66
|
+
options,
|
|
67
|
+
{
|
|
68
|
+
projectRoot,
|
|
69
|
+
reactNativePath: userConfig.reactNativePath,
|
|
70
|
+
// below: the userConfig.reactNativeVersion may be a non-semver-format string,
|
|
71
|
+
// e.g. '0.82' (note the missing patch component),
|
|
72
|
+
// therefore we resolve it manually from RN's package.json using Rock's utils
|
|
73
|
+
reactNativeVersion: getReactNativeVersion(projectRoot),
|
|
74
|
+
usePrebuiltRNCore: false, // for brownfield, it is required to build RN from source
|
|
75
|
+
packageDir, // the output directory for artifacts
|
|
76
|
+
skipCache: true, // cache is dependent on existence of Rock config file
|
|
77
|
+
},
|
|
78
|
+
platformConfig
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (hasBrownie) {
|
|
82
|
+
const productsPath = path.join(options.buildFolder, 'Build', 'Products');
|
|
83
|
+
const brownieOutputPath = path.join(packageDir, 'Brownie.xcframework');
|
|
84
|
+
|
|
85
|
+
await mergeFrameworks({
|
|
86
|
+
sourceDir: userConfig.project.ios.sourceDir,
|
|
87
|
+
frameworkPaths: [
|
|
88
|
+
path.join(
|
|
89
|
+
productsPath,
|
|
90
|
+
`${configuration}-iphoneos`,
|
|
91
|
+
'Brownie',
|
|
92
|
+
'Brownie.framework'
|
|
93
|
+
),
|
|
94
|
+
path.join(
|
|
95
|
+
productsPath,
|
|
96
|
+
`${configuration}-iphonesimulator`,
|
|
97
|
+
'Brownie',
|
|
98
|
+
'Brownie.framework'
|
|
99
|
+
),
|
|
100
|
+
],
|
|
101
|
+
outputPath: brownieOutputPath,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
logger.success(
|
|
105
|
+
`Brownie.xcframework created at ${colorLink(relativeToCwd(brownieOutputPath))}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
export const packageIosExample = new ExampleUsage(
|
|
112
|
+
'package:ios --scheme BrownfieldLib --configuration Release',
|
|
113
|
+
"Build iOS XCFramework for 'BrownfieldLib' scheme in Release configuration"
|
|
114
|
+
);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { publishLocalAarAction } from '@rock-js/plugin-brownfield-android';
|
|
2
|
+
import {
|
|
3
|
+
publishLocalAarOptions,
|
|
4
|
+
type PublishLocalAarFlags,
|
|
5
|
+
} from '@rock-js/platform-android';
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
|
|
9
|
+
import { getProjectInfo } from '../utils/index.js';
|
|
10
|
+
import {
|
|
11
|
+
actionRunner,
|
|
12
|
+
curryOptions,
|
|
13
|
+
ExampleUsage,
|
|
14
|
+
} from '../../shared/index.js';
|
|
15
|
+
|
|
16
|
+
export const publishAndroidCommand = curryOptions(
|
|
17
|
+
new Command('publish:android').description(
|
|
18
|
+
'Publish Android package to Maven local'
|
|
19
|
+
),
|
|
20
|
+
publishLocalAarOptions
|
|
21
|
+
).action(
|
|
22
|
+
actionRunner(async (options: PublishLocalAarFlags) => {
|
|
23
|
+
const { projectRoot, platformConfig } = getProjectInfo('android');
|
|
24
|
+
|
|
25
|
+
await publishLocalAarAction({
|
|
26
|
+
projectRoot,
|
|
27
|
+
pluginConfig: platformConfig,
|
|
28
|
+
moduleName: options.moduleName,
|
|
29
|
+
});
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const publishAndroidExample = new ExampleUsage(
|
|
34
|
+
'publish:android --module-name :BrownfieldLib',
|
|
35
|
+
"Publish all built variants for 'BrownfieldLib' module to Maven local"
|
|
36
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import {
|
|
3
|
+
packageAndroidCommand,
|
|
4
|
+
packageAndroidExample,
|
|
5
|
+
} from './commands/packageAndroid.js';
|
|
6
|
+
import {
|
|
7
|
+
publishAndroidCommand,
|
|
8
|
+
publishAndroidExample,
|
|
9
|
+
} from './commands/publishAndroid.js';
|
|
10
|
+
import { packageIosCommand, packageIosExample } from './commands/packageIos.js';
|
|
11
|
+
|
|
12
|
+
export * from './utils/index.js';
|
|
13
|
+
|
|
14
|
+
export const groupName = `${styleText(['bold', 'blueBright'], '@callstack/react-native-brownfield')}${styleText('whiteBright', ' - utilities for React Native Brownfield projects')}`;
|
|
15
|
+
|
|
16
|
+
export const Commands = {
|
|
17
|
+
packageAndroidCommand,
|
|
18
|
+
packageAndroidExample,
|
|
19
|
+
publishAndroidCommand,
|
|
20
|
+
publishAndroidExample,
|
|
21
|
+
packageIosCommand,
|
|
22
|
+
packageIosExample,
|
|
23
|
+
};
|
|
24
|
+
export default Commands;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
AndroidProjectConfig,
|
|
6
|
+
IOSProjectConfig,
|
|
7
|
+
} from '@react-native-community/cli-types';
|
|
8
|
+
import cloneDeep from 'lodash.clonedeep';
|
|
9
|
+
|
|
10
|
+
export function makeRelativeProjectConfigPaths<
|
|
11
|
+
UserConfig extends AndroidProjectConfig | IOSProjectConfig | undefined,
|
|
12
|
+
>(projectRoot: string, userConfig: UserConfig): UserConfig {
|
|
13
|
+
const relativeConfig = cloneDeep(userConfig);
|
|
14
|
+
|
|
15
|
+
if (userConfig?.sourceDir) {
|
|
16
|
+
relativeConfig!.sourceDir = path.relative(
|
|
17
|
+
projectRoot,
|
|
18
|
+
userConfig.sourceDir
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return relativeConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Helper function to find RN project root by recursively looking for a package.json in the parent directories
|
|
27
|
+
* @returns The path to the project root directory
|
|
28
|
+
*/
|
|
29
|
+
export function findProjectRoot(): string {
|
|
30
|
+
let currentDir = process.cwd();
|
|
31
|
+
|
|
32
|
+
while (currentDir !== '/') {
|
|
33
|
+
if (fs.existsSync(path.join(currentDir, 'package.json'))) {
|
|
34
|
+
return currentDir;
|
|
35
|
+
}
|
|
36
|
+
currentDir = path.dirname(currentDir);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new Error('Could not find project root (no package.json found)');
|
|
40
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { styleText } from 'node:util';
|
|
3
|
+
|
|
4
|
+
import { Command, Option } from 'commander';
|
|
5
|
+
|
|
6
|
+
import { intro, logger, outro } from '@rock-js/tools';
|
|
7
|
+
import { QuickTypeError } from 'quicktype-core';
|
|
8
|
+
import { actionRunner } from '../../shared/index.js';
|
|
9
|
+
import {
|
|
10
|
+
loadConfig,
|
|
11
|
+
getSwiftOutputPath,
|
|
12
|
+
type BrownieConfig,
|
|
13
|
+
} from '../config.js';
|
|
14
|
+
import { generateSwift } from '../generators/swift.js';
|
|
15
|
+
import { generateKotlin } from '../generators/kotlin.js';
|
|
16
|
+
import { discoverStores, type DiscoveredStore } from '../store-discovery.js';
|
|
17
|
+
import type { Platform } from '../types.js';
|
|
18
|
+
|
|
19
|
+
function getOutputPath(dir: string, name: string, ext: string): string {
|
|
20
|
+
return path.join(dir, `${name}.${ext}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatQuickTypeError(error: QuickTypeError): string {
|
|
24
|
+
let message = error.errorMessage;
|
|
25
|
+
for (const [key, value] of Object.entries(error.properties)) {
|
|
26
|
+
message = message.replaceAll('${' + key + '}', value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return message;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function generateForStore(
|
|
33
|
+
store: DiscoveredStore,
|
|
34
|
+
config: BrownieConfig,
|
|
35
|
+
platforms: Platform[],
|
|
36
|
+
showLabel: boolean
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
const { name, schemaPath } = store;
|
|
39
|
+
const storeLabel = showLabel ? ` [${name}]` : '';
|
|
40
|
+
|
|
41
|
+
logger.info(`Generating types for store ${name}`);
|
|
42
|
+
|
|
43
|
+
for (const p of platforms) {
|
|
44
|
+
let outputPath: string;
|
|
45
|
+
|
|
46
|
+
if (p === 'swift') {
|
|
47
|
+
const swiftOutputDir = getSwiftOutputPath();
|
|
48
|
+
outputPath = getOutputPath(swiftOutputDir, name, 'swift');
|
|
49
|
+
} else {
|
|
50
|
+
const kotlinOutputDir = config.kotlin;
|
|
51
|
+
if (!kotlinOutputDir) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
outputPath = getOutputPath(kotlinOutputDir, name, 'kt');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
if (p === 'swift') {
|
|
59
|
+
await generateSwift({
|
|
60
|
+
name,
|
|
61
|
+
schemaPath,
|
|
62
|
+
typeName: name,
|
|
63
|
+
outputPath,
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
await generateKotlin({
|
|
67
|
+
name,
|
|
68
|
+
schemaPath,
|
|
69
|
+
typeName: name,
|
|
70
|
+
outputPath,
|
|
71
|
+
packageName: config.kotlinPackageName,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
logger.success(
|
|
75
|
+
`Generated ${outputPath}${styleText('italic', storeLabel)}`
|
|
76
|
+
);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger.error(
|
|
79
|
+
`Error generating ${p}${storeLabel}: ${error instanceof QuickTypeError ? formatQuickTypeError(error) : error instanceof Error ? error.message : error}`
|
|
80
|
+
);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type RunCodegenOptions = { platform?: Platform };
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Runs the codegen command with the given arguments.
|
|
90
|
+
*/
|
|
91
|
+
export async function runCodegen({ platform }: RunCodegenOptions) {
|
|
92
|
+
intro(
|
|
93
|
+
`Running Brownie codegen for ${platform ? `platform ${platform}` : 'all platforms'}`
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const config = loadConfig();
|
|
97
|
+
|
|
98
|
+
if (platform && !['swift', 'kotlin'].includes(platform)) {
|
|
99
|
+
logger.error(`Invalid platform: ${platform}. Must be 'swift' or 'kotlin'`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const stores = discoverStores();
|
|
104
|
+
const isMultipleStores = stores.length > 1;
|
|
105
|
+
const schemaList = stores.map((s) => path.basename(s.schemaPath)).join(', ');
|
|
106
|
+
|
|
107
|
+
logger.info(
|
|
108
|
+
styleText('cyan', `Generating store types from ${schemaList}...`)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
for (const store of stores) {
|
|
112
|
+
let platforms: Platform[];
|
|
113
|
+
|
|
114
|
+
if (platform) {
|
|
115
|
+
platforms = [platform];
|
|
116
|
+
} else {
|
|
117
|
+
// Only generate Swift by default (Kotlin not yet released)
|
|
118
|
+
platforms = ['swift'];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await generateForStore(store, config, platforms, isMultipleStores);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
outro('Done!');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const codegenCommand = new Command('codegen')
|
|
128
|
+
.description('Generate native store types from TypeScript schema')
|
|
129
|
+
.addOption(
|
|
130
|
+
new Option(
|
|
131
|
+
'-p, --platform <platform>',
|
|
132
|
+
'Generate for specific platform (swift)'
|
|
133
|
+
).choices(['swift'])
|
|
134
|
+
)
|
|
135
|
+
.action(
|
|
136
|
+
actionRunner(async (options: RunCodegenOptions) => {
|
|
137
|
+
await runCodegen(options);
|
|
138
|
+
})
|
|
139
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './codegen.js';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
|
|
5
|
+
export interface BrownieConfig {
|
|
6
|
+
kotlin?: string;
|
|
7
|
+
kotlinPackageName?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface PackageJson {
|
|
11
|
+
brownie?: BrownieConfig;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Checks if @callstack/brownie package is installed.
|
|
16
|
+
*/
|
|
17
|
+
export function isBrownieInstalled(
|
|
18
|
+
projectRoot: string = process.cwd()
|
|
19
|
+
): boolean {
|
|
20
|
+
const require = createRequire(path.join(projectRoot, 'package.json'));
|
|
21
|
+
try {
|
|
22
|
+
require.resolve('@callstack/brownie/package.json');
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the path to the @callstack/brownie package.
|
|
31
|
+
*/
|
|
32
|
+
export function getBrowniePackagePath(
|
|
33
|
+
projectRoot: string = process.cwd()
|
|
34
|
+
): string {
|
|
35
|
+
const require = createRequire(path.join(projectRoot, 'package.json'));
|
|
36
|
+
try {
|
|
37
|
+
const browniePackageJson =
|
|
38
|
+
require.resolve('@callstack/brownie/package.json');
|
|
39
|
+
return path.dirname(browniePackageJson);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"@callstack/brownie is not installed. Run 'npm install @callstack/brownie' or 'yarn add @callstack/brownie'"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns the output path for generated Swift files.
|
|
49
|
+
*/
|
|
50
|
+
export function getSwiftOutputPath(
|
|
51
|
+
projectRoot: string = process.cwd()
|
|
52
|
+
): string {
|
|
53
|
+
const browniePath = getBrowniePackagePath(projectRoot);
|
|
54
|
+
return path.join(browniePath, 'ios', 'Generated');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Loads brownie config from package.json in the current working directory.
|
|
59
|
+
*/
|
|
60
|
+
export function loadConfig(): BrownieConfig {
|
|
61
|
+
const packageJsonPath = path.resolve(process.cwd(), 'package.json');
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
64
|
+
throw new Error('package.json not found');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const packageJson: PackageJson = JSON.parse(
|
|
68
|
+
fs.readFileSync(packageJsonPath, 'utf-8')
|
|
69
|
+
);
|
|
70
|
+
return packageJson.brownie ?? {};
|
|
71
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
quicktype,
|
|
5
|
+
InputData,
|
|
6
|
+
JSONSchemaInput,
|
|
7
|
+
FetchingJSONSchemaStore,
|
|
8
|
+
} from 'quicktype-core';
|
|
9
|
+
import { schemaForTypeScriptSources } from 'quicktype-typescript-input';
|
|
10
|
+
|
|
11
|
+
export interface KotlinGeneratorOptions {
|
|
12
|
+
name: string;
|
|
13
|
+
schemaPath: string;
|
|
14
|
+
typeName: string;
|
|
15
|
+
outputPath: string;
|
|
16
|
+
packageName?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extracts Kotlin package name from output path.
|
|
21
|
+
* e.g. "./kotlin/app/src/main/java/com/example/generated/BrownfieldStore.kt" -> "com.example.generated"
|
|
22
|
+
*/
|
|
23
|
+
function extractPackageName(outputPath: string): string {
|
|
24
|
+
const javaIndex = outputPath.indexOf('/java/');
|
|
25
|
+
if (javaIndex === -1) {
|
|
26
|
+
return 'generated';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const packagePath = outputPath.slice(javaIndex + 6);
|
|
30
|
+
const parts = packagePath.split('/');
|
|
31
|
+
parts.pop();
|
|
32
|
+
return parts.join('.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generates Kotlin data class from TypeScript schema.
|
|
37
|
+
*/
|
|
38
|
+
export async function generateKotlin(
|
|
39
|
+
options: KotlinGeneratorOptions
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const {
|
|
42
|
+
name,
|
|
43
|
+
schemaPath,
|
|
44
|
+
typeName,
|
|
45
|
+
outputPath,
|
|
46
|
+
packageName: configPackageName,
|
|
47
|
+
} = options;
|
|
48
|
+
|
|
49
|
+
const absoluteSchemaPath = path.resolve(process.cwd(), schemaPath);
|
|
50
|
+
|
|
51
|
+
if (!fs.existsSync(absoluteSchemaPath)) {
|
|
52
|
+
throw new Error(`Schema file not found: ${absoluteSchemaPath}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const schemaData = schemaForTypeScriptSources([absoluteSchemaPath]);
|
|
56
|
+
if (!schemaData.schema) {
|
|
57
|
+
throw new Error('Failed to generate schema from TypeScript');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const parsedSchema = JSON.parse(schemaData.schema);
|
|
61
|
+
const definitions = parsedSchema.definitions as
|
|
62
|
+
| Record<string, unknown>
|
|
63
|
+
| undefined;
|
|
64
|
+
|
|
65
|
+
if (!definitions?.[typeName]) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Type "${typeName}" not found in schema. Available types: ${Object.keys(definitions || {}).join(', ')}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
parsedSchema.$ref = `#/definitions/${typeName}`;
|
|
72
|
+
const modifiedSchema = JSON.stringify(parsedSchema);
|
|
73
|
+
|
|
74
|
+
const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore());
|
|
75
|
+
await schemaInput.addSource({
|
|
76
|
+
name: typeName,
|
|
77
|
+
schema: modifiedSchema,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const inputData = new InputData();
|
|
81
|
+
inputData.addInput(schemaInput);
|
|
82
|
+
|
|
83
|
+
const absoluteOutputPath = path.resolve(process.cwd(), outputPath);
|
|
84
|
+
const packageName =
|
|
85
|
+
configPackageName ?? extractPackageName(absoluteOutputPath);
|
|
86
|
+
|
|
87
|
+
const { lines } = await quicktype({
|
|
88
|
+
inputData,
|
|
89
|
+
lang: 'kotlin',
|
|
90
|
+
rendererOptions: {
|
|
91
|
+
framework: 'just-types',
|
|
92
|
+
package: packageName,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
let kotlinOutput = lines.join('\n');
|
|
97
|
+
|
|
98
|
+
const companionObject = ` {
|
|
99
|
+
companion object {
|
|
100
|
+
const val STORE_NAME = "${name}"
|
|
101
|
+
}
|
|
102
|
+
}`;
|
|
103
|
+
const classPattern = new RegExp(`(data class ${typeName}\\s*\\([^)]*\\))`);
|
|
104
|
+
kotlinOutput = kotlinOutput.replace(classPattern, `$1${companionObject}`);
|
|
105
|
+
|
|
106
|
+
const outputDir = path.dirname(absoluteOutputPath);
|
|
107
|
+
|
|
108
|
+
if (!fs.existsSync(outputDir)) {
|
|
109
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fs.writeFileSync(absoluteOutputPath, kotlinOutput);
|
|
113
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
quicktype,
|
|
5
|
+
InputData,
|
|
6
|
+
JSONSchemaInput,
|
|
7
|
+
FetchingJSONSchemaStore,
|
|
8
|
+
} from 'quicktype-core';
|
|
9
|
+
import { schemaForTypeScriptSources } from 'quicktype-typescript-input';
|
|
10
|
+
|
|
11
|
+
export interface SwiftGeneratorOptions {
|
|
12
|
+
name: string;
|
|
13
|
+
schemaPath: string;
|
|
14
|
+
typeName: string;
|
|
15
|
+
outputPath: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generates Swift Codable struct from TypeScript schema.
|
|
20
|
+
*/
|
|
21
|
+
export async function generateSwift(
|
|
22
|
+
options: SwiftGeneratorOptions
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const { name, schemaPath, typeName, outputPath } = options;
|
|
25
|
+
|
|
26
|
+
const absoluteSchemaPath = path.resolve(process.cwd(), schemaPath);
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(absoluteSchemaPath)) {
|
|
29
|
+
throw new Error(`Schema file not found: ${absoluteSchemaPath}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const schemaData = schemaForTypeScriptSources([absoluteSchemaPath]);
|
|
33
|
+
if (!schemaData.schema) {
|
|
34
|
+
throw new Error('Failed to generate schema from TypeScript');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const parsedSchema = JSON.parse(schemaData.schema);
|
|
38
|
+
const definitions = parsedSchema.definitions as
|
|
39
|
+
| Record<string, unknown>
|
|
40
|
+
| undefined;
|
|
41
|
+
|
|
42
|
+
if (!definitions?.[typeName]) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Type "${typeName}" not found in schema. Available types: ${Object.keys(definitions || {}).join(', ')}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
parsedSchema.$ref = `#/definitions/${typeName}`;
|
|
49
|
+
const modifiedSchema = JSON.stringify(parsedSchema);
|
|
50
|
+
|
|
51
|
+
const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore());
|
|
52
|
+
await schemaInput.addSource({
|
|
53
|
+
name: typeName,
|
|
54
|
+
schema: modifiedSchema,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const inputData = new InputData();
|
|
58
|
+
inputData.addInput(schemaInput);
|
|
59
|
+
|
|
60
|
+
const { lines } = await quicktype({
|
|
61
|
+
inputData,
|
|
62
|
+
lang: 'swift',
|
|
63
|
+
rendererOptions: {
|
|
64
|
+
'access-level': 'public',
|
|
65
|
+
'mutable-properties': 'true',
|
|
66
|
+
initializers: 'false',
|
|
67
|
+
'swift-5-support': 'true',
|
|
68
|
+
protocol: 'equatable',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const storeNameExtension = `
|
|
73
|
+
extension ${typeName}: BrownieStoreProtocol {
|
|
74
|
+
public static let storeName = "${name}"
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
const swiftOutput = lines.join('\n') + storeNameExtension;
|
|
79
|
+
const absoluteOutputPath = path.resolve(process.cwd(), outputPath);
|
|
80
|
+
const outputDir = path.dirname(absoluteOutputPath);
|
|
81
|
+
|
|
82
|
+
if (!fs.existsSync(outputDir)) {
|
|
83
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fs.writeFileSync(absoluteOutputPath, swiftOutput);
|
|
87
|
+
}
|