voltra 2.0.0 → 2.1.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 (84) hide show
  1. package/build/cjs/config/normalize.js +93 -0
  2. package/build/cjs/config/normalize.js.map +1 -1
  3. package/build/cjs/index.js +5 -2
  4. package/build/cjs/index.js.map +1 -1
  5. package/build/cjs/platforms/android/apply.js +11 -2
  6. package/build/cjs/platforms/android/apply.js.map +1 -1
  7. package/build/cjs/platforms/android/generated.js +152 -130
  8. package/build/cjs/platforms/android/generated.js.map +1 -1
  9. package/build/cjs/platforms/android/gradle.js +120 -0
  10. package/build/cjs/platforms/android/gradle.js.map +1 -0
  11. package/build/cjs/platforms/ios/generated.js +246 -142
  12. package/build/cjs/platforms/ios/generated.js.map +1 -1
  13. package/build/cjs/platforms/ios/xcodeTarget.js +68 -0
  14. package/build/cjs/platforms/ios/xcodeTarget.js.map +1 -1
  15. package/build/cjs/platforms/shared/widgetModule.js +151 -0
  16. package/build/cjs/platforms/shared/widgetModule.js.map +1 -0
  17. package/build/esm/config/normalize.js +93 -0
  18. package/build/esm/config/normalize.js.map +1 -1
  19. package/build/esm/index.js +1 -0
  20. package/build/esm/index.js.map +1 -1
  21. package/build/esm/platforms/android/apply.js +11 -2
  22. package/build/esm/platforms/android/apply.js.map +1 -1
  23. package/build/esm/platforms/android/generated.js +152 -97
  24. package/build/esm/platforms/android/generated.js.map +1 -1
  25. package/build/esm/platforms/android/gradle.js +112 -0
  26. package/build/esm/platforms/android/gradle.js.map +1 -0
  27. package/build/esm/platforms/ios/generated.js +246 -109
  28. package/build/esm/platforms/ios/generated.js.map +1 -1
  29. package/build/esm/platforms/ios/xcodeTarget.js +69 -1
  30. package/build/esm/platforms/ios/xcodeTarget.js.map +1 -1
  31. package/build/esm/platforms/shared/widgetModule.js +111 -0
  32. package/build/esm/platforms/shared/widgetModule.js.map +1 -0
  33. package/build/types/config/normalize.d.ts.map +1 -1
  34. package/build/types/config/types.d.ts +32 -0
  35. package/build/types/config/types.d.ts.map +1 -1
  36. package/build/types/index.d.ts +3 -1
  37. package/build/types/index.d.ts.map +1 -1
  38. package/build/types/platforms/android/apply.d.ts.map +1 -1
  39. package/build/types/platforms/android/generated.d.ts.map +1 -1
  40. package/build/types/platforms/android/gradle.d.ts +18 -0
  41. package/build/types/platforms/android/gradle.d.ts.map +1 -0
  42. package/build/types/platforms/ios/generated.d.ts.map +1 -1
  43. package/build/types/platforms/ios/xcodeTarget.d.ts.map +1 -1
  44. package/build/types/platforms/shared/widgetModule.d.ts +9 -0
  45. package/build/types/platforms/shared/widgetModule.d.ts.map +1 -0
  46. package/package.json +2 -1
  47. package/src/apply/index.ts +309 -0
  48. package/src/apply/preflight.ts +172 -0
  49. package/src/bin.ts +14 -0
  50. package/src/commands/apply.ts +114 -0
  51. package/src/config/defaults.ts +34 -0
  52. package/src/config/load.ts +100 -0
  53. package/src/config/normalize.ts +562 -0
  54. package/src/config/types.ts +317 -0
  55. package/src/cosmiconfig.d.ts +21 -0
  56. package/src/dependencies/platformPackages.ts +75 -0
  57. package/src/discovery/android.ts +233 -0
  58. package/src/discovery/ios.ts +579 -0
  59. package/src/fs/path.ts +25 -0
  60. package/src/fs/readWrite.ts +102 -0
  61. package/src/git/status.ts +205 -0
  62. package/src/index.ts +204 -0
  63. package/src/platforms/android/apply.ts +106 -0
  64. package/src/platforms/android/generated.ts +1284 -0
  65. package/src/platforms/android/gradle.ts +155 -0
  66. package/src/platforms/android/manifest.ts +379 -0
  67. package/src/platforms/ios/apply.ts +139 -0
  68. package/src/platforms/ios/deploymentTarget.ts +47 -0
  69. package/src/platforms/ios/entitlements.ts +146 -0
  70. package/src/platforms/ios/generated.ts +1233 -0
  71. package/src/platforms/ios/mainAppEntitlements.ts +11 -0
  72. package/src/platforms/ios/plist.ts +321 -0
  73. package/src/platforms/ios/podfile.ts +255 -0
  74. package/src/platforms/ios/targetName.ts +11 -0
  75. package/src/platforms/ios/xcode.ts +181 -0
  76. package/src/platforms/ios/xcodeTarget.ts +899 -0
  77. package/src/platforms/shared/widgetModule.ts +153 -0
  78. package/src/reporting/clack.ts +67 -0
  79. package/src/reporting/summary.ts +116 -0
  80. package/src/state/diff.ts +21 -0
  81. package/src/state/files.ts +52 -0
  82. package/src/state/load.ts +66 -0
  83. package/src/state/save.ts +31 -0
  84. package/src/xml2js.d.ts +23 -0
@@ -0,0 +1,205 @@
1
+ import { execFile } from 'node:child_process'
2
+ import type { Readable, Writable } from 'node:stream'
3
+ import { promisify } from 'node:util'
4
+
5
+ import { renderCancelled, renderWarning } from '../reporting/clack'
6
+ import { formatDirtyWorktreeWarning, VoltraCliError } from '../reporting/summary'
7
+
8
+ import type * as ClackPrompts from '@clack/prompts'
9
+
10
+ const execFileAsync = promisify(execFile)
11
+
12
+ const GIT_NOT_REPOSITORY_EXIT_CODE = 128
13
+ export interface GitWorktreeStatus {
14
+ isGitRepository: boolean
15
+ isDirty: boolean
16
+ repoRoot?: string
17
+ entries: string[]
18
+ }
19
+
20
+ export interface EnsureGitWorktreeOptions {
21
+ cwd: string
22
+ interactive?: boolean
23
+ allowDirty?: boolean
24
+ stdin?: Readable & { isTTY?: boolean }
25
+ stdout?: Writable & { isTTY?: boolean }
26
+ }
27
+
28
+ export interface EnsureGitWorktreeResult {
29
+ status: GitWorktreeStatus
30
+ warning?: string
31
+ }
32
+
33
+ interface ExecGitOptions {
34
+ cwd: string
35
+ }
36
+
37
+ class GitCommandError extends VoltraCliError {
38
+ readonly args: string[]
39
+ readonly exitCode?: number
40
+ readonly stderr?: string
41
+
42
+ constructor(args: string[], cause: unknown) {
43
+ const message = cause instanceof Error ? cause.message : String(cause)
44
+ super(`Failed to run git ${args.join(' ')}: ${message}`)
45
+ this.name = 'GitCommandError'
46
+ this.args = args
47
+ this.exitCode = getExecExitCode(cause)
48
+ this.stderr = getExecStderr(cause)
49
+ }
50
+ }
51
+
52
+ export async function getGitWorktreeStatus(cwd: string): Promise<GitWorktreeStatus> {
53
+ const insideWorktree = await runGitCommand(['rev-parse', '--is-inside-work-tree'], { cwd }).catch(
54
+ (error: unknown) => {
55
+ if (isNotGitRepositoryError(error)) {
56
+ return undefined
57
+ }
58
+
59
+ throw error
60
+ }
61
+ )
62
+
63
+ if (!insideWorktree || insideWorktree.trim() !== 'true') {
64
+ return {
65
+ isGitRepository: false,
66
+ isDirty: false,
67
+ entries: [],
68
+ }
69
+ }
70
+
71
+ const [repoRootOutput, statusOutput] = await Promise.all([
72
+ runGitCommand(['rev-parse', '--show-toplevel'], { cwd }),
73
+ runGitCommand(['status', '--short', '--untracked-files=normal'], { cwd }),
74
+ ])
75
+
76
+ const entries = splitGitStatusEntries(statusOutput)
77
+
78
+ return {
79
+ isGitRepository: true,
80
+ isDirty: entries.length > 0,
81
+ repoRoot: repoRootOutput.trim(),
82
+ entries,
83
+ }
84
+ }
85
+
86
+ export async function ensureGitWorktreeIsReady(options: EnsureGitWorktreeOptions): Promise<EnsureGitWorktreeResult> {
87
+ const status = await getGitWorktreeStatus(options.cwd)
88
+
89
+ if (!status.isGitRepository || !status.isDirty) {
90
+ return { status }
91
+ }
92
+
93
+ const warning = formatDirtyWorktreeWarning(status.entries.length)
94
+
95
+ if (options.allowDirty) {
96
+ return { status, warning }
97
+ }
98
+
99
+ if (!isInteractiveSession(options)) {
100
+ throw new VoltraCliError(`${warning} Re-run interactively to confirm before applying changes.`)
101
+ }
102
+
103
+ const confirmed = await promptForDirtyWorktreeConfirmation(options)
104
+
105
+ if (!confirmed) {
106
+ throw new VoltraCliError('Aborted because the git worktree has uncommitted changes.')
107
+ }
108
+
109
+ return { status, warning }
110
+ }
111
+
112
+ async function runGitCommand(args: string[], options: ExecGitOptions): Promise<string> {
113
+ try {
114
+ const result = await execFileAsync('git', args, {
115
+ cwd: options.cwd,
116
+ encoding: 'utf8',
117
+ })
118
+
119
+ return result.stdout
120
+ } catch (error: unknown) {
121
+ throw new GitCommandError(args, error)
122
+ }
123
+ }
124
+
125
+ function isNotGitRepositoryError(error: unknown): boolean {
126
+ if (!(error instanceof GitCommandError)) {
127
+ return false
128
+ }
129
+
130
+ return (
131
+ error.args.join(' ') === 'rev-parse --is-inside-work-tree' &&
132
+ error.exitCode === GIT_NOT_REPOSITORY_EXIT_CODE &&
133
+ typeof error.stderr === 'string' &&
134
+ error.stderr.toLowerCase().includes('not a git repository')
135
+ )
136
+ }
137
+
138
+ function getExecExitCode(error: unknown): number | undefined {
139
+ if (!(error instanceof Error) || !('code' in error)) {
140
+ return undefined
141
+ }
142
+
143
+ const code = error.code
144
+
145
+ return typeof code === 'number' ? code : undefined
146
+ }
147
+
148
+ function getExecStderr(error: unknown): string | undefined {
149
+ if (!(error instanceof Error) || !('stderr' in error)) {
150
+ return undefined
151
+ }
152
+
153
+ const stderr = error.stderr
154
+
155
+ return typeof stderr === 'string' ? stderr : undefined
156
+ }
157
+
158
+ function splitGitStatusEntries(output: string): string[] {
159
+ return output
160
+ .split(/\r?\n/)
161
+ .map((entry) => entry.trimEnd())
162
+ .filter((entry) => entry.length > 0)
163
+ }
164
+
165
+ function isInteractiveSession(options: EnsureGitWorktreeOptions): boolean {
166
+ if (options.interactive !== undefined) {
167
+ return options.interactive
168
+ }
169
+
170
+ const stdin = options.stdin ?? process.stdin
171
+ const stdout = options.stdout ?? process.stdout
172
+
173
+ return Boolean(stdin.isTTY && stdout.isTTY)
174
+ }
175
+
176
+ async function promptForDirtyWorktreeConfirmation(options: EnsureGitWorktreeOptions): Promise<boolean> {
177
+ const stdin = options.stdin ?? process.stdin
178
+ const stdout = options.stdout ?? process.stdout
179
+ const promptMessage = 'You have uncommitted changes. Are you sure you want to continue?'
180
+ const { confirm, isCancel } = await loadClackPrompts()
181
+
182
+ await renderWarning(promptMessage, { input: stdin, output: stdout })
183
+
184
+ const response = await confirm({
185
+ message: 'Continue?',
186
+ initialValue: false,
187
+ input: stdin,
188
+ output: stdout,
189
+ })
190
+
191
+ if (isCancel(response)) {
192
+ await renderCancelled({ input: stdin, output: stdout })
193
+ return false
194
+ }
195
+
196
+ return response === true
197
+ }
198
+
199
+ function loadClackPrompts() {
200
+ const dynamicImport = new Function('specifier', 'return import(specifier)') as (
201
+ specifier: string
202
+ ) => Promise<typeof ClackPrompts>
203
+
204
+ return dynamicImport('@clack/prompts')
205
+ }
package/src/index.ts ADDED
@@ -0,0 +1,204 @@
1
+ import { Command } from 'commander'
2
+
3
+ import { CLI_EXIT_CODE_FAILURE, CLI_EXIT_CODE_SUCCESS, createApplyCommand, formatCommandError } from './commands/apply'
4
+ import { renderError } from './reporting/clack'
5
+
6
+ function createProgram(): Command {
7
+ const program = new Command()
8
+ let exitCode = CLI_EXIT_CODE_SUCCESS
9
+
10
+ program
11
+ .name('voltra')
12
+ .description('Apply Voltra to native React Native projects')
13
+ .exitOverride()
14
+ .showHelpAfterError()
15
+ .showSuggestionAfterError()
16
+ .addHelpText(
17
+ 'after',
18
+ [
19
+ '',
20
+ 'Examples:',
21
+ ' voltra apply',
22
+ ' voltra apply --platform ios',
23
+ ' voltra apply --config ./voltra.config.ts --yes',
24
+ ].join('\n')
25
+ )
26
+
27
+ program.addCommand(
28
+ createApplyCommand((nextExitCode) => {
29
+ exitCode = nextExitCode
30
+ })
31
+ )
32
+
33
+ program.hook('postAction', () => {
34
+ program.setOptionValue('exitCode', exitCode)
35
+ })
36
+
37
+ return program
38
+ }
39
+
40
+ export async function runCli(argv: string[]): Promise<number> {
41
+ const program = createProgram()
42
+
43
+ try {
44
+ await program.parseAsync(['node', 'voltra', ...argv], { from: 'node' })
45
+ return program.getOptionValue('exitCode') ?? CLI_EXIT_CODE_SUCCESS
46
+ } catch (error) {
47
+ if (isCommanderError(error)) {
48
+ return isCommanderDisplayError(error) ? CLI_EXIT_CODE_SUCCESS : CLI_EXIT_CODE_FAILURE
49
+ }
50
+
51
+ await renderError(formatCommandError(error), { output: process.stderr })
52
+ return CLI_EXIT_CODE_FAILURE
53
+ }
54
+ }
55
+
56
+ function isCommanderDisplayError(error: unknown): error is { code: string } {
57
+ return Boolean(
58
+ error &&
59
+ typeof error === 'object' &&
60
+ 'code' in error &&
61
+ (error.code === 'commander.helpDisplayed' || error.code === 'commander.version')
62
+ )
63
+ }
64
+
65
+ function isCommanderError(error: unknown): error is { code: string } {
66
+ return Boolean(
67
+ error &&
68
+ typeof error === 'object' &&
69
+ 'code' in error &&
70
+ typeof error.code === 'string' &&
71
+ error.code.startsWith('commander.')
72
+ )
73
+ }
74
+
75
+ export { applyVoltra, runApplyPipeline } from './apply'
76
+ export type {
77
+ ApplyDependencies,
78
+ ApplyOptions,
79
+ ApplyResult,
80
+ PlatformApplyContext,
81
+ PlatformApplyResult,
82
+ PlatformApplyRunner,
83
+ } from './apply'
84
+ export { getRequestedPlatforms, runApplyPreflight } from './apply/preflight'
85
+ export type {
86
+ ApplyPreflightContext,
87
+ ApplyPreflightResult,
88
+ ApplyPreflightRunners,
89
+ PlatformPreflightFailure,
90
+ PlatformPreflightResult,
91
+ PlatformPreflightRunner,
92
+ PlatformPreflightSuccess,
93
+ } from './apply/preflight'
94
+ export { CLI_EXIT_CODE_FAILURE, CLI_EXIT_CODE_SUCCESS, getApplyHelpText, runApplyCommand } from './commands/apply'
95
+
96
+ export { CLI_DEFAULTS } from './config/defaults'
97
+ export { VoltraConfigLoadError, loadVoltraConfig } from './config/load'
98
+ export { VoltraConfigNormalizationError, normalizeVoltraConfig } from './config/normalize'
99
+ export { AndroidProjectDiscoveryError, discoverAndroidProject } from './discovery/android'
100
+ export type { AndroidProjectDiscovery } from './discovery/android'
101
+ export { IOSProjectDiscoveryError, discoverIOSProject } from './discovery/ios'
102
+ export type { IOSProjectDiscovery } from './discovery/ios'
103
+ export { ensureGitWorktreeIsReady, getGitWorktreeStatus } from './git/status'
104
+ export type { EnsureGitWorktreeOptions, EnsureGitWorktreeResult, GitWorktreeStatus } from './git/status'
105
+ export { AndroidGeneratedFilesError, generateAndroidFiles } from './platforms/android/generated'
106
+ export type { GenerateAndroidFilesOptions, GenerateAndroidFilesResult } from './platforms/android/generated'
107
+ export { applyAndroidPlatform, createAndroidPreflightRunner } from './platforms/android/apply'
108
+ export { addDynamicWidgetBundlingSnippet, ensureAndroidGradleWidgetBundling } from './platforms/android/gradle'
109
+ export type {
110
+ EnsureAndroidGradleWidgetBundlingOptions,
111
+ EnsureAndroidGradleWidgetBundlingResult,
112
+ } from './platforms/android/gradle'
113
+ export { AndroidManifestMutationError, ensureAndroidManifest } from './platforms/android/manifest'
114
+ export type { EnsureAndroidManifestOptions, EnsureAndroidManifestResult } from './platforms/android/manifest'
115
+ export { IOSGeneratedFilesError, generateIOSFiles } from './platforms/ios/generated'
116
+ export type { GenerateIOSFilesOptions, GenerateIOSFilesResult } from './platforms/ios/generated'
117
+ export { resolveIOSWidgetTargetName } from './platforms/ios/targetName'
118
+ export { IOSWidgetTargetMutationError, ensureIOSWidgetTarget } from './platforms/ios/xcodeTarget'
119
+ export { applyIOSPlatform, createIOSPreflightRunner } from './platforms/ios/apply'
120
+ export { IOSEntitlementsMutationError, ensureEntitlements } from './platforms/ios/entitlements'
121
+ export type { EnsureEntitlementsOptions, EnsureEntitlementsResult } from './platforms/ios/entitlements'
122
+ export { IOSInfoPlistMutationError, ensureInfoPlist } from './platforms/ios/plist'
123
+ export type { EnsureInfoPlistOptions, EnsureInfoPlistResult } from './platforms/ios/plist'
124
+ export { IOSPodfileMutationError, ensurePodfileBlock } from './platforms/ios/podfile'
125
+ export type { EnsurePodfileBlockOptions, EnsurePodfileBlockResult } from './platforms/ios/podfile'
126
+ export type { EnsureIOSWidgetTargetOptions, EnsureIOSWidgetTargetResult } from './platforms/ios/xcodeTarget'
127
+ export {
128
+ ensureFrameworksGroup,
129
+ ensureMainGroupChild,
130
+ ensureProductsGroup,
131
+ getApplicationTargets,
132
+ getTargetBuildConfigurations,
133
+ getTargetBuildPhases,
134
+ IOSXcodeProjectError,
135
+ openIOSXcodeProject,
136
+ saveIOSXcodeProject,
137
+ } from './platforms/ios/xcode'
138
+ export type {
139
+ IOSXcodeProjectContext,
140
+ IOSXcodeTargetBuildConfigurations,
141
+ IOSXcodeTargetBuildPhases,
142
+ IOSXcodeTargetContext,
143
+ } from './platforms/ios/xcode'
144
+ export { diffVoltraState } from './state/diff'
145
+ export { getVoltraStatePath, loadVoltraState } from './state/load'
146
+ export { saveVoltraState } from './state/save'
147
+ export {
148
+ normalizeClackMessage,
149
+ renderApplySummary,
150
+ renderCancelled,
151
+ renderError,
152
+ renderWarning,
153
+ } from './reporting/clack'
154
+ export type { VoltraStateDiff } from './state/diff'
155
+ export type { SaveVoltraStateInput } from './state/save'
156
+ export type { VoltraState } from './state/load'
157
+ export {
158
+ formatAmbiguousDiscoveryWarning,
159
+ formatApplySummary,
160
+ formatDirtyWorktreeWarning,
161
+ formatError,
162
+ formatPreflightFailure,
163
+ formatWarning,
164
+ PreflightError,
165
+ VoltraCliError,
166
+ } from './reporting/summary'
167
+ export type {
168
+ AndroidProjectOverrides,
169
+ AndroidWidgetAppIntentConfig,
170
+ AndroidWidgetAppIntentParameter,
171
+ AndroidWidgetConfig,
172
+ AndroidWidgetServerUpdateConfig,
173
+ CliDefaults,
174
+ IOSProjectOverrides,
175
+ IOSWidgetAppIntentConfig,
176
+ IOSWidgetAppIntentParameter,
177
+ IOSWidgetConfig,
178
+ IOSWidgetFamily,
179
+ IOSWidgetServerUpdateConfig,
180
+ LoadedVoltraConfig,
181
+ NormalizedAndroidProjectConfig,
182
+ NormalizedAndroidWidgetConfig,
183
+ NormalizedAndroidWidgetServerUpdateConfig,
184
+ NormalizedIOSProjectConfig,
185
+ NormalizedIOSWidgetConfig,
186
+ NormalizedIOSWidgetServerUpdateConfig,
187
+ NormalizedVoltraAndroidConfig,
188
+ NormalizedVoltraConfig,
189
+ NormalizedVoltraIOSConfig,
190
+ VoltraAndroidConfig,
191
+ VoltraConfig,
192
+ VoltraIOSConfig,
193
+ VoltraPlatform,
194
+ WidgetInitialStatePath,
195
+ WidgetLabel,
196
+ WidgetLocalizedValue,
197
+ } from './config/types'
198
+ export type {
199
+ ApplySummary,
200
+ PreflightFailureReport,
201
+ PreflightIssue,
202
+ ReportedChange,
203
+ ReportedChangeKind,
204
+ } from './reporting/summary'
@@ -0,0 +1,106 @@
1
+ import { discoverAndroidProject } from '../../discovery/android'
2
+ import { getMissingPlatformPackageMessage, getMissingPlatformPackages } from '../../dependencies/platformPackages'
3
+ import { VoltraCliError } from '../../reporting/summary'
4
+
5
+ import { ensureAndroidManifest } from './manifest'
6
+ import { ensureAndroidGradleWidgetBundling } from './gradle'
7
+ import { generateAndroidFiles } from './generated'
8
+
9
+ import type { NormalizedVoltraConfig } from '../../config/types'
10
+ import type { AndroidProjectDiscovery } from '../../discovery/android'
11
+ import type { PlatformApplyContext, PlatformApplyResult } from '../../apply'
12
+ import type { ApplyPreflightContext, PlatformPreflightResult, PlatformPreflightRunner } from '../../apply/preflight'
13
+
14
+ export function createAndroidPreflightRunner(
15
+ config: NormalizedVoltraConfig
16
+ ): PlatformPreflightRunner<AndroidProjectDiscovery> {
17
+ return async (_context: ApplyPreflightContext): Promise<PlatformPreflightResult<AndroidProjectDiscovery>> => {
18
+ const androidConfig = config.android
19
+
20
+ if (!androidConfig) {
21
+ return {
22
+ platform: 'android',
23
+ issues: [{ message: 'Android config is missing.' }],
24
+ }
25
+ }
26
+
27
+ const missingPackages = getMissingPlatformPackages(config.projectRoot, 'android')
28
+
29
+ if (missingPackages.length > 0) {
30
+ return {
31
+ platform: 'android',
32
+ issues: [{ message: getMissingPlatformPackageMessage('android', missingPackages) }],
33
+ }
34
+ }
35
+
36
+ return {
37
+ platform: 'android',
38
+ context: await discoverAndroidProject(config.projectRoot, androidConfig.project),
39
+ }
40
+ }
41
+ }
42
+
43
+ export async function applyAndroidPlatform(context: PlatformApplyContext): Promise<PlatformApplyResult> {
44
+ if (context.platform !== 'android') {
45
+ throw new VoltraCliError(`Android apply runner received unexpected platform: ${context.platform}.`)
46
+ }
47
+
48
+ const androidConfig = context.config.android
49
+
50
+ if (!androidConfig) {
51
+ throw new VoltraCliError('Android config is missing.')
52
+ }
53
+
54
+ const discovery = getAndroidDiscovery(context.preflight)
55
+ const generatedResult = await generateAndroidFiles({
56
+ projectRoot: context.config.projectRoot,
57
+ android: androidConfig,
58
+ discovery,
59
+ })
60
+ const manifestResult = await ensureAndroidManifest({
61
+ projectRoot: context.config.projectRoot,
62
+ android: androidConfig,
63
+ discovery,
64
+ })
65
+ const gradleResult = await ensureAndroidGradleWidgetBundling({
66
+ projectRoot: context.config.projectRoot,
67
+ discovery,
68
+ hasDynamicWidgets: androidConfig.widgets.some((widget) => widget.entry !== undefined),
69
+ })
70
+
71
+ return {
72
+ platform: 'android',
73
+ changes: [manifestResult.change, gradleResult.change, ...generatedResult.changes].filter(isDefined),
74
+ generatedFiles: generatedResult.files,
75
+ warnings: [...generatedResult.warnings, ...(gradleResult.warnings ?? [])],
76
+ }
77
+ }
78
+
79
+ function getAndroidDiscovery(value: unknown): AndroidProjectDiscovery {
80
+ if (!isAndroidProjectDiscovery(value)) {
81
+ throw new VoltraCliError('Android preflight did not provide a valid discovery result.')
82
+ }
83
+
84
+ return value
85
+ }
86
+
87
+ function isAndroidProjectDiscovery(value: unknown): value is AndroidProjectDiscovery {
88
+ if (!value || typeof value !== 'object') {
89
+ return false
90
+ }
91
+
92
+ const candidate = value as Partial<AndroidProjectDiscovery>
93
+
94
+ return [
95
+ candidate.androidRoot,
96
+ candidate.appModuleName,
97
+ candidate.appModuleRoot,
98
+ candidate.manifestPath,
99
+ candidate.buildGradlePath,
100
+ candidate.packageName,
101
+ ].every((entry) => typeof entry === 'string' && entry.length > 0)
102
+ }
103
+
104
+ function isDefined<TValue>(value: TValue | undefined): value is TValue {
105
+ return value !== undefined
106
+ }