@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.
Files changed (98) hide show
  1. package/dist/brownfield/commands/index.d.ts +4 -0
  2. package/dist/brownfield/commands/index.d.ts.map +1 -0
  3. package/dist/brownfield/commands/index.js +3 -0
  4. package/dist/brownfield/commands/packageAndroid.d.ts +5 -0
  5. package/dist/brownfield/commands/packageAndroid.d.ts.map +1 -0
  6. package/dist/brownfield/commands/packageAndroid.js +17 -0
  7. package/dist/brownfield/commands/packageIos.d.ts +5 -0
  8. package/dist/brownfield/commands/packageIos.d.ts.map +1 -0
  9. package/dist/brownfield/commands/packageIos.js +57 -0
  10. package/dist/brownfield/commands/publishAndroid.d.ts +5 -0
  11. package/dist/brownfield/commands/publishAndroid.d.ts.map +1 -0
  12. package/dist/brownfield/commands/publishAndroid.js +14 -0
  13. package/dist/brownfield/index.d.ts +12 -0
  14. package/dist/brownfield/index.d.ts.map +1 -0
  15. package/dist/brownfield/index.js +15 -0
  16. package/dist/brownfield/types/actionRunner.test.d.ts +2 -0
  17. package/dist/brownfield/types/actionRunner.test.d.ts.map +1 -0
  18. package/dist/brownfield/types/actionRunner.test.js +1 -0
  19. package/dist/brownfield/utils/index.d.ts +3 -0
  20. package/dist/brownfield/utils/index.d.ts.map +1 -0
  21. package/dist/brownfield/utils/index.js +2 -0
  22. package/dist/brownfield/utils/paths.d.ts +8 -0
  23. package/dist/brownfield/utils/paths.d.ts.map +1 -0
  24. package/dist/brownfield/utils/paths.js +24 -0
  25. package/dist/brownfield/utils/rn-cli.d.ts +17 -0
  26. package/dist/brownfield/utils/rn-cli.d.ts.map +1 -0
  27. package/dist/brownfield/utils/rn-cli.js +31 -0
  28. package/dist/brownie/commands/codegen.d.ts +11 -0
  29. package/dist/brownie/commands/codegen.d.ts.map +1 -0
  30. package/dist/brownie/commands/codegen.js +96 -0
  31. package/dist/brownie/commands/index.d.ts +2 -0
  32. package/dist/brownie/commands/index.d.ts.map +1 -0
  33. package/dist/brownie/commands/index.js +1 -0
  34. package/dist/brownie/config.d.ts +21 -0
  35. package/dist/brownie/config.d.ts.map +1 -0
  36. package/dist/brownie/config.js +47 -0
  37. package/dist/brownie/generators/kotlin.d.ts +12 -0
  38. package/dist/brownie/generators/kotlin.d.ts.map +1 -0
  39. package/dist/brownie/generators/kotlin.js +69 -0
  40. package/dist/brownie/generators/swift.d.ts +11 -0
  41. package/dist/brownie/generators/swift.d.ts.map +1 -0
  42. package/dist/brownie/generators/swift.js +55 -0
  43. package/dist/brownie/index.d.ts +7 -0
  44. package/dist/brownie/index.d.ts.map +1 -0
  45. package/dist/brownie/index.js +6 -0
  46. package/dist/brownie/store-discovery.d.ts +10 -0
  47. package/dist/brownie/store-discovery.d.ts.map +1 -0
  48. package/dist/brownie/store-discovery.js +75 -0
  49. package/dist/brownie/types.d.ts +2 -0
  50. package/dist/brownie/types.d.ts.map +1 -0
  51. package/dist/brownie/types.js +1 -0
  52. package/dist/cli.d.ts +3 -0
  53. package/dist/cli.d.ts.map +1 -0
  54. package/dist/cli.js +54 -0
  55. package/dist/index.d.ts +2 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +55 -0
  58. package/dist/main.d.ts +3 -0
  59. package/dist/main.d.ts.map +1 -0
  60. package/dist/main.js +3 -0
  61. package/dist/shared/classes/ExampleUsage.d.ts +6 -0
  62. package/dist/shared/classes/ExampleUsage.d.ts.map +1 -0
  63. package/dist/shared/classes/ExampleUsage.js +8 -0
  64. package/dist/shared/classes/index.d.ts +2 -0
  65. package/dist/shared/classes/index.d.ts.map +1 -0
  66. package/dist/shared/classes/index.js +1 -0
  67. package/dist/shared/index.d.ts +3 -0
  68. package/dist/shared/index.d.ts.map +1 -0
  69. package/dist/shared/index.js +2 -0
  70. package/dist/shared/utils/cli.d.ts +5 -0
  71. package/dist/shared/utils/cli.d.ts.map +1 -0
  72. package/dist/shared/utils/cli.js +36 -0
  73. package/dist/shared/utils/index.d.ts +2 -0
  74. package/dist/shared/utils/index.d.ts.map +1 -0
  75. package/dist/shared/utils/index.js +1 -0
  76. package/package.json +98 -0
  77. package/src/brownfield/commands/packageAndroid.ts +39 -0
  78. package/src/brownfield/commands/packageIos.ts +114 -0
  79. package/src/brownfield/commands/publishAndroid.ts +36 -0
  80. package/src/brownfield/index.ts +24 -0
  81. package/src/brownfield/utils/index.ts +2 -0
  82. package/src/brownfield/utils/paths.ts +40 -0
  83. package/src/brownfield/utils/rn-cli.ts +58 -0
  84. package/src/brownie/commands/codegen.ts +139 -0
  85. package/src/brownie/commands/index.ts +1 -0
  86. package/src/brownie/config.ts +71 -0
  87. package/src/brownie/generators/kotlin.ts +113 -0
  88. package/src/brownie/generators/swift.ts +87 -0
  89. package/src/brownie/index.ts +11 -0
  90. package/src/brownie/store-discovery.ts +108 -0
  91. package/src/brownie/types.ts +1 -0
  92. package/src/index.ts +84 -0
  93. package/src/main.ts +5 -0
  94. package/src/shared/classes/ExampleUsage.ts +6 -0
  95. package/src/shared/classes/index.ts +1 -0
  96. package/src/shared/index.ts +2 -0
  97. package/src/shared/utils/cli.ts +47 -0
  98. 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,2 @@
1
+ export * from './paths.js';
2
+ export * from './rn-cli.js';
@@ -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
+ }