voltra 2.0.0 → 2.1.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/build/cjs/config/normalize.js +93 -0
- package/build/cjs/config/normalize.js.map +1 -1
- package/build/cjs/index.js +5 -2
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/platforms/android/apply.js +11 -2
- package/build/cjs/platforms/android/apply.js.map +1 -1
- package/build/cjs/platforms/android/generated.js +152 -130
- package/build/cjs/platforms/android/generated.js.map +1 -1
- package/build/cjs/platforms/android/gradle.js +120 -0
- package/build/cjs/platforms/android/gradle.js.map +1 -0
- package/build/cjs/platforms/ios/generated.js +246 -142
- package/build/cjs/platforms/ios/generated.js.map +1 -1
- package/build/cjs/platforms/ios/xcodeTarget.js +68 -0
- package/build/cjs/platforms/ios/xcodeTarget.js.map +1 -1
- package/build/cjs/platforms/shared/widgetModule.js +151 -0
- package/build/cjs/platforms/shared/widgetModule.js.map +1 -0
- package/build/esm/config/normalize.js +93 -0
- package/build/esm/config/normalize.js.map +1 -1
- package/build/esm/index.js +1 -0
- package/build/esm/index.js.map +1 -1
- package/build/esm/platforms/android/apply.js +11 -2
- package/build/esm/platforms/android/apply.js.map +1 -1
- package/build/esm/platforms/android/generated.js +152 -97
- package/build/esm/platforms/android/generated.js.map +1 -1
- package/build/esm/platforms/android/gradle.js +112 -0
- package/build/esm/platforms/android/gradle.js.map +1 -0
- package/build/esm/platforms/ios/generated.js +246 -109
- package/build/esm/platforms/ios/generated.js.map +1 -1
- package/build/esm/platforms/ios/xcodeTarget.js +69 -1
- package/build/esm/platforms/ios/xcodeTarget.js.map +1 -1
- package/build/esm/platforms/shared/widgetModule.js +111 -0
- package/build/esm/platforms/shared/widgetModule.js.map +1 -0
- package/build/types/config/normalize.d.ts.map +1 -1
- package/build/types/config/types.d.ts +32 -0
- package/build/types/config/types.d.ts.map +1 -1
- package/build/types/index.d.ts +3 -1
- package/build/types/index.d.ts.map +1 -1
- package/build/types/platforms/android/apply.d.ts.map +1 -1
- package/build/types/platforms/android/generated.d.ts.map +1 -1
- package/build/types/platforms/android/gradle.d.ts +18 -0
- package/build/types/platforms/android/gradle.d.ts.map +1 -0
- package/build/types/platforms/ios/generated.d.ts.map +1 -1
- package/build/types/platforms/ios/xcodeTarget.d.ts.map +1 -1
- package/build/types/platforms/shared/widgetModule.d.ts +9 -0
- package/build/types/platforms/shared/widgetModule.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/apply/index.ts +309 -0
- package/src/apply/preflight.ts +172 -0
- package/src/bin.ts +14 -0
- package/src/commands/apply.ts +114 -0
- package/src/config/defaults.ts +34 -0
- package/src/config/load.ts +100 -0
- package/src/config/normalize.ts +562 -0
- package/src/config/types.ts +317 -0
- package/src/cosmiconfig.d.ts +21 -0
- package/src/dependencies/platformPackages.ts +75 -0
- package/src/discovery/android.ts +233 -0
- package/src/discovery/ios.ts +579 -0
- package/src/fs/path.ts +25 -0
- package/src/fs/readWrite.ts +102 -0
- package/src/git/status.ts +205 -0
- package/src/index.ts +204 -0
- package/src/platforms/android/apply.ts +106 -0
- package/src/platforms/android/generated.ts +1284 -0
- package/src/platforms/android/gradle.ts +155 -0
- package/src/platforms/android/manifest.ts +379 -0
- package/src/platforms/ios/apply.ts +139 -0
- package/src/platforms/ios/deploymentTarget.ts +47 -0
- package/src/platforms/ios/entitlements.ts +146 -0
- package/src/platforms/ios/generated.ts +1233 -0
- package/src/platforms/ios/mainAppEntitlements.ts +11 -0
- package/src/platforms/ios/plist.ts +321 -0
- package/src/platforms/ios/podfile.ts +255 -0
- package/src/platforms/ios/targetName.ts +11 -0
- package/src/platforms/ios/xcode.ts +181 -0
- package/src/platforms/ios/xcodeTarget.ts +899 -0
- package/src/platforms/shared/widgetModule.ts +153 -0
- package/src/reporting/clack.ts +67 -0
- package/src/reporting/summary.ts +116 -0
- package/src/state/diff.ts +21 -0
- package/src/state/files.ts +52 -0
- package/src/state/load.ts +66 -0
- package/src/state/save.ts +31 -0
- package/src/xml2js.d.ts +23 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { normalizeRelativePath } from '../fs/path'
|
|
4
|
+
import { loadVoltraConfig } from '../config/load'
|
|
5
|
+
import { normalizeVoltraConfig } from '../config/normalize'
|
|
6
|
+
import { removePathIfExists } from '../fs/readWrite'
|
|
7
|
+
import { ensureGitWorktreeIsReady } from '../git/status'
|
|
8
|
+
import { applyAndroidPlatform, createAndroidPreflightRunner } from '../platforms/android/apply'
|
|
9
|
+
import { applyIOSPlatform, createIOSPreflightRunner } from '../platforms/ios/apply'
|
|
10
|
+
import { renderApplySummary, renderIntro } from '../reporting/clack'
|
|
11
|
+
import { VoltraCliError } from '../reporting/summary'
|
|
12
|
+
import { diffVoltraState } from '../state/diff'
|
|
13
|
+
import { loadVoltraState } from '../state/load'
|
|
14
|
+
import { saveVoltraState } from '../state/save'
|
|
15
|
+
|
|
16
|
+
import { runApplyPreflight } from './preflight'
|
|
17
|
+
|
|
18
|
+
import type { NormalizedVoltraConfig, VoltraPlatform } from '../config/types'
|
|
19
|
+
import type { ApplyPreflightResult, ApplyPreflightRunners } from './preflight'
|
|
20
|
+
import type { ReportedChange } from '../reporting/summary'
|
|
21
|
+
import type { VoltraState } from '../state/load'
|
|
22
|
+
|
|
23
|
+
export interface ApplyOptions {
|
|
24
|
+
configPath?: string
|
|
25
|
+
platform?: VoltraPlatform
|
|
26
|
+
allowDirty?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ApplyResult {
|
|
30
|
+
exitCode: number
|
|
31
|
+
errorMessage?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PlatformApplyContext {
|
|
35
|
+
config: NormalizedVoltraConfig
|
|
36
|
+
platform: VoltraPlatform
|
|
37
|
+
preflight: unknown
|
|
38
|
+
previousState?: VoltraState
|
|
39
|
+
requestedPlatforms: VoltraPlatform[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface PlatformApplyResult {
|
|
43
|
+
platform: VoltraPlatform
|
|
44
|
+
changes: ReportedChange[]
|
|
45
|
+
generatedFiles: string[]
|
|
46
|
+
warnings?: string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type PlatformApplyRunner = (context: PlatformApplyContext) => Promise<PlatformApplyResult>
|
|
50
|
+
|
|
51
|
+
export interface ApplyDependencies {
|
|
52
|
+
applyRunners: Partial<Record<VoltraPlatform, PlatformApplyRunner>>
|
|
53
|
+
preflightRunners: ApplyPreflightRunners
|
|
54
|
+
writeIntro(): Promise<void>
|
|
55
|
+
writeSummary(summary: { changes: ReportedChange[]; warnings?: string[] }): Promise<void>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DEFAULT_DEPENDENCIES: ApplyDependencies = {
|
|
59
|
+
applyRunners: {},
|
|
60
|
+
preflightRunners: {},
|
|
61
|
+
async writeIntro() {
|
|
62
|
+
await renderIntro()
|
|
63
|
+
},
|
|
64
|
+
async writeSummary(summary) {
|
|
65
|
+
await renderApplySummary(summary)
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function applyVoltra(options: ApplyOptions): Promise<ApplyResult> {
|
|
70
|
+
try {
|
|
71
|
+
await runApplyPipeline(options, DEFAULT_DEPENDENCIES)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
exitCode: 0,
|
|
75
|
+
}
|
|
76
|
+
} catch (error: unknown) {
|
|
77
|
+
return {
|
|
78
|
+
exitCode: 1,
|
|
79
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function runApplyPipeline(options: ApplyOptions, dependencies: ApplyDependencies): Promise<void> {
|
|
85
|
+
const loadedConfig = await loadVoltraConfig({ configPath: options.configPath })
|
|
86
|
+
const normalizedConfig = normalizeVoltraConfig(loadedConfig)
|
|
87
|
+
const resolvedDependencies = resolveApplyDependencies(normalizedConfig, dependencies)
|
|
88
|
+
await resolvedDependencies.writeIntro()
|
|
89
|
+
await ensureGitWorktreeIsReady({
|
|
90
|
+
cwd: normalizedConfig.projectRoot,
|
|
91
|
+
allowDirty: options.allowDirty,
|
|
92
|
+
})
|
|
93
|
+
const preflight = await runApplyPreflight(normalizedConfig, resolvedDependencies.preflightRunners, options.platform)
|
|
94
|
+
const previousState = await loadVoltraState(normalizedConfig.projectRoot)
|
|
95
|
+
const platformResults = await runPlatformApply(
|
|
96
|
+
normalizedConfig,
|
|
97
|
+
preflight,
|
|
98
|
+
previousState,
|
|
99
|
+
resolvedDependencies.applyRunners
|
|
100
|
+
)
|
|
101
|
+
const nextGeneratedFiles = mergeGeneratedFiles(
|
|
102
|
+
normalizedConfig,
|
|
103
|
+
previousState,
|
|
104
|
+
preflight.requestedPlatforms,
|
|
105
|
+
platformResults
|
|
106
|
+
)
|
|
107
|
+
const stateDiff = diffVoltraState(previousState, nextGeneratedFiles)
|
|
108
|
+
const deletedChanges = await removeStaleGeneratedFiles(normalizedConfig.projectRoot, stateDiff.staleFiles)
|
|
109
|
+
await saveVoltraState(normalizedConfig.projectRoot, { files: stateDiff.nextFiles })
|
|
110
|
+
|
|
111
|
+
const summaryWarnings = platformResults.flatMap((result) => result.warnings ?? []).filter(isDefined)
|
|
112
|
+
const summaryChanges = [...platformResults.flatMap((result) => result.changes), ...deletedChanges]
|
|
113
|
+
|
|
114
|
+
await resolvedDependencies.writeSummary({ changes: summaryChanges, warnings: summaryWarnings })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveApplyDependencies(config: NormalizedVoltraConfig, dependencies: ApplyDependencies): ApplyDependencies {
|
|
118
|
+
return {
|
|
119
|
+
applyRunners: {
|
|
120
|
+
android: dependencies.applyRunners.android ?? applyAndroidPlatform,
|
|
121
|
+
ios: dependencies.applyRunners.ios ?? applyIOSPlatform,
|
|
122
|
+
},
|
|
123
|
+
preflightRunners: {
|
|
124
|
+
android:
|
|
125
|
+
dependencies.preflightRunners.android ?? (config.android ? createAndroidPreflightRunner(config) : undefined),
|
|
126
|
+
ios: dependencies.preflightRunners.ios ?? (config.ios ? createIOSPreflightRunner(config) : undefined),
|
|
127
|
+
},
|
|
128
|
+
writeIntro: dependencies.writeIntro,
|
|
129
|
+
writeSummary: dependencies.writeSummary,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function mergeGeneratedFiles(
|
|
134
|
+
config: NormalizedVoltraConfig,
|
|
135
|
+
previousState: VoltraState | undefined,
|
|
136
|
+
requestedPlatforms: VoltraPlatform[],
|
|
137
|
+
platformResults: PlatformApplyResult[]
|
|
138
|
+
): string[] {
|
|
139
|
+
const nextGeneratedFilesByPlatform = new Map(
|
|
140
|
+
platformResults.map((result) => [result.platform, result.generatedFiles] as const)
|
|
141
|
+
)
|
|
142
|
+
const mergedFiles = new Set<string>()
|
|
143
|
+
const platformRoots = getTrackedPlatformRoots(config)
|
|
144
|
+
const configuredPlatforms = getConfiguredPlatforms(config)
|
|
145
|
+
const isPartialApply = requestedPlatforms.length < configuredPlatforms.length
|
|
146
|
+
|
|
147
|
+
for (const previousFile of previousState?.files ?? []) {
|
|
148
|
+
const owningPlatform = getTrackedFilePlatform(previousFile, platformRoots)
|
|
149
|
+
|
|
150
|
+
if (!owningPlatform) {
|
|
151
|
+
if (isPartialApply) {
|
|
152
|
+
mergedFiles.add(previousFile)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (requestedPlatforms.includes(owningPlatform)) {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
mergedFiles.add(previousFile)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const platform of requestedPlatforms) {
|
|
166
|
+
for (const filePath of nextGeneratedFilesByPlatform.get(platform) ?? []) {
|
|
167
|
+
mergedFiles.add(filePath)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return [...mergedFiles]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getTrackedPlatformRoots(config: NormalizedVoltraConfig): Partial<Record<VoltraPlatform, string>> {
|
|
175
|
+
const projectRoot = config.projectRoot
|
|
176
|
+
const androidRoot = config.android
|
|
177
|
+
? normalizeRelativePath(
|
|
178
|
+
path.relative(projectRoot, config.android.project.rootDir ?? path.join(projectRoot, 'android'))
|
|
179
|
+
)
|
|
180
|
+
: undefined
|
|
181
|
+
const iosRoot = config.ios
|
|
182
|
+
? normalizeRelativePath(path.relative(projectRoot, config.ios.project.rootDir ?? path.join(projectRoot, 'ios')))
|
|
183
|
+
: undefined
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
...(androidRoot ? { android: androidRoot } : {}),
|
|
187
|
+
...(iosRoot ? { ios: iosRoot } : {}),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getConfiguredPlatforms(config: NormalizedVoltraConfig): VoltraPlatform[] {
|
|
192
|
+
const platforms: VoltraPlatform[] = []
|
|
193
|
+
|
|
194
|
+
if (config.android) {
|
|
195
|
+
platforms.push('android')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (config.ios) {
|
|
199
|
+
platforms.push('ios')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return platforms
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getTrackedFilePlatform(
|
|
206
|
+
filePath: string,
|
|
207
|
+
platformRoots: Partial<Record<VoltraPlatform, string>>
|
|
208
|
+
): VoltraPlatform | undefined {
|
|
209
|
+
const normalizedFilePath = normalizeRelativePath(filePath)
|
|
210
|
+
|
|
211
|
+
for (const platform of ['android', 'ios'] as const) {
|
|
212
|
+
const root = platformRoots[platform]
|
|
213
|
+
|
|
214
|
+
if (!root) {
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (normalizedFilePath === root || normalizedFilePath.startsWith(`${root}/`)) {
|
|
219
|
+
return platform
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return undefined
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function runPlatformApply(
|
|
227
|
+
config: NormalizedVoltraConfig,
|
|
228
|
+
preflight: ApplyPreflightResult,
|
|
229
|
+
previousState: VoltraState | undefined,
|
|
230
|
+
applyRunners: Partial<Record<VoltraPlatform, PlatformApplyRunner>>
|
|
231
|
+
): Promise<PlatformApplyResult[]> {
|
|
232
|
+
const missingRunners = preflight.requestedPlatforms.filter((platform) => !applyRunners[platform])
|
|
233
|
+
|
|
234
|
+
if (missingRunners.length > 0) {
|
|
235
|
+
throw new VoltraCliError(`No apply runner is registered for ${missingRunners.join(', ')}.`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const results: PlatformApplyResult[] = []
|
|
239
|
+
|
|
240
|
+
for (const platform of preflight.requestedPlatforms) {
|
|
241
|
+
const applyRunner = applyRunners[platform]
|
|
242
|
+
|
|
243
|
+
if (!applyRunner) {
|
|
244
|
+
throw new VoltraCliError(`No apply runner is registered for ${platform}.`)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let result: PlatformApplyResult
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
result = await applyRunner({
|
|
251
|
+
config,
|
|
252
|
+
platform,
|
|
253
|
+
preflight: preflight.platformResults[platform],
|
|
254
|
+
previousState,
|
|
255
|
+
requestedPlatforms: preflight.requestedPlatforms,
|
|
256
|
+
})
|
|
257
|
+
} catch (error: unknown) {
|
|
258
|
+
throw new VoltraCliError(`Apply failed for ${platform}: ${getApplyRunnerErrorMessage(error)}`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (result.platform !== platform) {
|
|
262
|
+
throw new VoltraCliError(
|
|
263
|
+
`Apply runner returned a mismatched platform result: expected ${platform}, received ${result.platform}.`
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
results.push(result)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return results
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function removeStaleGeneratedFiles(projectRoot: string, staleFiles: string[]): Promise<ReportedChange[]> {
|
|
274
|
+
const deletedChanges: ReportedChange[] = []
|
|
275
|
+
|
|
276
|
+
for (const staleFile of staleFiles) {
|
|
277
|
+
const staleFilePath = path.join(projectRoot, staleFile)
|
|
278
|
+
let deleted: boolean
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
deleted = await removePathIfExists(staleFilePath)
|
|
282
|
+
} catch (error: unknown) {
|
|
283
|
+
throw new VoltraCliError(
|
|
284
|
+
`Failed to remove stale generated file ${staleFile}: ${getApplyRunnerErrorMessage(error)}`
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (deleted) {
|
|
289
|
+
deletedChanges.push({
|
|
290
|
+
kind: 'deleted',
|
|
291
|
+
path: staleFile,
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return deletedChanges
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isDefined<TValue>(value: TValue | undefined): value is TValue {
|
|
300
|
+
return value !== undefined
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function getApplyRunnerErrorMessage(error: unknown): string {
|
|
304
|
+
if (error instanceof Error && error.message) {
|
|
305
|
+
return error.message
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return String(error)
|
|
309
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { PreflightError } from '../reporting/summary'
|
|
2
|
+
|
|
3
|
+
import type { NormalizedVoltraConfig, VoltraPlatform } from '../config/types'
|
|
4
|
+
import type { PreflightFailureReport, PreflightIssue } from '../reporting/summary'
|
|
5
|
+
|
|
6
|
+
export interface PlatformPreflightSuccess<TContext> {
|
|
7
|
+
platform: VoltraPlatform
|
|
8
|
+
context: TContext
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PlatformPreflightFailure {
|
|
12
|
+
platform: VoltraPlatform
|
|
13
|
+
issues: PreflightIssue[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type PlatformPreflightResult<TContext> = PlatformPreflightSuccess<TContext> | PlatformPreflightFailure
|
|
17
|
+
|
|
18
|
+
export interface ApplyPreflightContext {
|
|
19
|
+
requestedPlatforms: VoltraPlatform[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type PlatformPreflightRunner<TContext> = (
|
|
23
|
+
context: ApplyPreflightContext
|
|
24
|
+
) => Promise<PlatformPreflightResult<TContext>>
|
|
25
|
+
|
|
26
|
+
export interface ApplyPreflightRunners {
|
|
27
|
+
android?: PlatformPreflightRunner<unknown>
|
|
28
|
+
ios?: PlatformPreflightRunner<unknown>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ApplyPreflightResult {
|
|
32
|
+
requestedPlatforms: VoltraPlatform[]
|
|
33
|
+
platformResults: Partial<Record<VoltraPlatform, unknown>>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getRequestedPlatforms(config: NormalizedVoltraConfig, platform?: VoltraPlatform): VoltraPlatform[] {
|
|
37
|
+
if (platform) {
|
|
38
|
+
validateConfiguredPlatform(config, platform)
|
|
39
|
+
return [platform]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const configuredPlatforms = getConfiguredPlatforms(config)
|
|
43
|
+
|
|
44
|
+
if (configuredPlatforms.length === 0) {
|
|
45
|
+
throw new PreflightError({
|
|
46
|
+
summary: 'No platforms are configured for Voltra apply.',
|
|
47
|
+
issues: [{ message: 'Add an android or ios config block before running apply.' }],
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return configuredPlatforms
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runApplyPreflight(
|
|
55
|
+
config: NormalizedVoltraConfig,
|
|
56
|
+
runners: ApplyPreflightRunners,
|
|
57
|
+
platform?: VoltraPlatform
|
|
58
|
+
): Promise<ApplyPreflightResult> {
|
|
59
|
+
const requestedPlatforms = getRequestedPlatforms(config, platform)
|
|
60
|
+
const preflightContext: ApplyPreflightContext = { requestedPlatforms }
|
|
61
|
+
const failures: PlatformPreflightFailure[] = []
|
|
62
|
+
const platformResults: Partial<Record<VoltraPlatform, unknown>> = {}
|
|
63
|
+
|
|
64
|
+
await Promise.all(
|
|
65
|
+
requestedPlatforms.map(async (requestedPlatform) => {
|
|
66
|
+
const runner = runners[requestedPlatform]
|
|
67
|
+
|
|
68
|
+
if (!runner) {
|
|
69
|
+
failures.push({
|
|
70
|
+
platform: requestedPlatform,
|
|
71
|
+
issues: [{ message: `No preflight runner is registered for ${requestedPlatform}.` }],
|
|
72
|
+
})
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let result: PlatformPreflightResult<unknown>
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
result = await runner(preflightContext)
|
|
80
|
+
} catch (error: unknown) {
|
|
81
|
+
failures.push({
|
|
82
|
+
platform: requestedPlatform,
|
|
83
|
+
issues: [{ message: getPreflightRunnerErrorMessage(error) }],
|
|
84
|
+
})
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if ('issues' in result) {
|
|
89
|
+
failures.push(result)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (result.platform !== requestedPlatform) {
|
|
94
|
+
failures.push({
|
|
95
|
+
platform: requestedPlatform,
|
|
96
|
+
issues: [{ message: `Preflight runner returned a mismatched platform result: ${result.platform}.` }],
|
|
97
|
+
})
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
platformResults[requestedPlatform] = result.context
|
|
102
|
+
})
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if (failures.length > 0) {
|
|
106
|
+
throw new PreflightError(buildPreflightFailureReport(failures))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
requestedPlatforms,
|
|
111
|
+
platformResults,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getConfiguredPlatforms(config: NormalizedVoltraConfig): VoltraPlatform[] {
|
|
116
|
+
const platforms: VoltraPlatform[] = []
|
|
117
|
+
|
|
118
|
+
if (config.android) {
|
|
119
|
+
platforms.push('android')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (config.ios) {
|
|
123
|
+
platforms.push('ios')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return platforms
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function validateConfiguredPlatform(config: NormalizedVoltraConfig, platform: VoltraPlatform): void {
|
|
130
|
+
if ((platform === 'android' && config.android) || (platform === 'ios' && config.ios)) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw new PreflightError({
|
|
135
|
+
summary: `Requested platform '${platform}' is not configured.`,
|
|
136
|
+
issues: [{ message: `Add a ${platform} config block or remove --platform ${platform}.` }],
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildPreflightFailureReport(failures: PlatformPreflightFailure[]): PreflightFailureReport {
|
|
141
|
+
const orderedFailures = [...failures].sort((left, right) => comparePlatforms(left.platform, right.platform))
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
summary: 'Preflight failed before any files were written.',
|
|
145
|
+
issues: orderedFailures.flatMap((failure) =>
|
|
146
|
+
failure.issues.map((issue) => ({
|
|
147
|
+
...issue,
|
|
148
|
+
message: `${failure.platform}: ${issue.message}`,
|
|
149
|
+
}))
|
|
150
|
+
),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getPreflightRunnerErrorMessage(error: unknown): string {
|
|
155
|
+
if (error instanceof Error && error.message) {
|
|
156
|
+
return error.message
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return String(error)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function comparePlatforms(left: VoltraPlatform, right: VoltraPlatform): number {
|
|
163
|
+
return getPlatformSortOrder(left) - getPlatformSortOrder(right)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getPlatformSortOrder(platform: VoltraPlatform): number {
|
|
167
|
+
if (platform === 'android') {
|
|
168
|
+
return 0
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return 1
|
|
172
|
+
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runCli } from './index'
|
|
4
|
+
|
|
5
|
+
void runCli(process.argv.slice(2))
|
|
6
|
+
.then((exitCode) => {
|
|
7
|
+
process.exitCode = exitCode
|
|
8
|
+
})
|
|
9
|
+
.catch((error: unknown) => {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
11
|
+
|
|
12
|
+
process.stderr.write(`${message}\n`)
|
|
13
|
+
process.exitCode = 1
|
|
14
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
|
|
3
|
+
import { applyVoltra } from '../apply'
|
|
4
|
+
import { normalizeClackMessage, renderError } from '../reporting/clack'
|
|
5
|
+
|
|
6
|
+
import type { ApplyOptions } from '../apply'
|
|
7
|
+
import type { VoltraPlatform } from '../config/types'
|
|
8
|
+
|
|
9
|
+
export const CLI_EXIT_CODE_SUCCESS = 0
|
|
10
|
+
export const CLI_EXIT_CODE_FAILURE = 1
|
|
11
|
+
|
|
12
|
+
interface ApplyCommandCliOptions {
|
|
13
|
+
platform?: VoltraPlatform
|
|
14
|
+
config?: string
|
|
15
|
+
yes?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runApplyCommand(argv: string[]): Promise<number> {
|
|
19
|
+
let exitCode = CLI_EXIT_CODE_SUCCESS
|
|
20
|
+
const command = createApplyCommand((nextExitCode) => {
|
|
21
|
+
exitCode = nextExitCode
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await command.parseAsync(argv, { from: 'user' })
|
|
26
|
+
return exitCode
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return await handleCommanderError(error)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createApplyCommand(onComplete?: (exitCode: number) => void): Command {
|
|
33
|
+
const command = new Command('apply')
|
|
34
|
+
|
|
35
|
+
command
|
|
36
|
+
.description('Apply Voltra changes to the native project')
|
|
37
|
+
.usage('[options]')
|
|
38
|
+
.exitOverride()
|
|
39
|
+
.showHelpAfterError()
|
|
40
|
+
.showSuggestionAfterError()
|
|
41
|
+
.option('-p, --platform <platform>', 'limit apply to a single platform', parsePlatform)
|
|
42
|
+
.option('-c, --config <path>', 'load config from an explicit file path')
|
|
43
|
+
.option('-y, --yes', 'skip the dirty git worktree confirmation prompt')
|
|
44
|
+
.action(async () => {
|
|
45
|
+
const cliOptions = command.opts<ApplyCommandCliOptions>()
|
|
46
|
+
const options = toApplyOptions(cliOptions)
|
|
47
|
+
const result = await applyVoltra(options)
|
|
48
|
+
|
|
49
|
+
onComplete?.(result.exitCode)
|
|
50
|
+
|
|
51
|
+
if (result.exitCode !== CLI_EXIT_CODE_SUCCESS && result.errorMessage) {
|
|
52
|
+
await renderError(result.errorMessage, { output: process.stderr })
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return command
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toApplyOptions(cliOptions: ApplyCommandCliOptions): ApplyOptions {
|
|
60
|
+
if (cliOptions.config !== undefined && !cliOptions.config.trim()) {
|
|
61
|
+
throw new Error('--config must be a non-empty path')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
platform: cliOptions.platform,
|
|
66
|
+
configPath: cliOptions.config,
|
|
67
|
+
allowDirty: cliOptions.yes,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parsePlatform(value: string): VoltraPlatform {
|
|
72
|
+
if (value === 'android' || value === 'ios') {
|
|
73
|
+
return value
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new Error(`Invalid platform '${value}'. Expected 'ios' or 'android'.`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function handleCommanderError(error: unknown): Promise<number> {
|
|
80
|
+
if (isCommanderError(error)) {
|
|
81
|
+
return isCommanderDisplayError(error) ? CLI_EXIT_CODE_SUCCESS : CLI_EXIT_CODE_FAILURE
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await renderError(formatCommandError(error), { output: process.stderr })
|
|
85
|
+
return CLI_EXIT_CODE_FAILURE
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isCommanderDisplayError(error: unknown): error is { code: string } {
|
|
89
|
+
return Boolean(
|
|
90
|
+
error &&
|
|
91
|
+
typeof error === 'object' &&
|
|
92
|
+
'code' in error &&
|
|
93
|
+
(error.code === 'commander.helpDisplayed' || error.code === 'commander.version')
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isCommanderError(error: unknown): error is { code: string } {
|
|
98
|
+
return Boolean(
|
|
99
|
+
error &&
|
|
100
|
+
typeof error === 'object' &&
|
|
101
|
+
'code' in error &&
|
|
102
|
+
typeof error.code === 'string' &&
|
|
103
|
+
error.code.startsWith('commander.')
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function formatCommandError(error: unknown): string {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
109
|
+
return normalizeClackMessage(message)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getApplyHelpText(): string {
|
|
113
|
+
return createApplyCommand().helpInformation()
|
|
114
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { IOSWidgetFamily } from './types'
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_ANDROID_ENABLE_NOTIFICATIONS = false
|
|
4
|
+
export const DEFAULT_ANDROID_SERVER_UPDATE_INTERVAL_MINUTES = 60
|
|
5
|
+
export const DEFAULT_ANDROID_SERVER_UPDATE_REFRESH = false
|
|
6
|
+
export const DEFAULT_ANDROID_USER_IMAGES_PATH = './assets/voltra-android'
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_IOS_ENABLE_PUSH_NOTIFICATIONS = false
|
|
9
|
+
export const DEFAULT_IOS_DEPLOYMENT_TARGET = '17.0'
|
|
10
|
+
export const DEFAULT_IOS_SERVER_UPDATE_INTERVAL_MINUTES = 15
|
|
11
|
+
export const DEFAULT_IOS_SERVER_UPDATE_REFRESH = false
|
|
12
|
+
export const DEFAULT_IOS_USER_IMAGES_PATH = './assets/voltra'
|
|
13
|
+
export const DEFAULT_IOS_WIDGET_FAMILIES: IOSWidgetFamily[] = ['systemSmall', 'systemMedium', 'systemLarge']
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* These defaults intentionally mirror the existing Expo plugin behavior where that behavior is already explicit.
|
|
17
|
+
* Some values, such as the default iOS widget target name, still depend on native project discovery and app naming.
|
|
18
|
+
*/
|
|
19
|
+
export const CLI_DEFAULTS = {
|
|
20
|
+
android: {
|
|
21
|
+
enableNotifications: DEFAULT_ANDROID_ENABLE_NOTIFICATIONS,
|
|
22
|
+
serverUpdateIntervalMinutes: DEFAULT_ANDROID_SERVER_UPDATE_INTERVAL_MINUTES,
|
|
23
|
+
serverUpdateRefresh: DEFAULT_ANDROID_SERVER_UPDATE_REFRESH,
|
|
24
|
+
userImagesPath: DEFAULT_ANDROID_USER_IMAGES_PATH,
|
|
25
|
+
},
|
|
26
|
+
ios: {
|
|
27
|
+
deploymentTarget: DEFAULT_IOS_DEPLOYMENT_TARGET,
|
|
28
|
+
enablePushNotifications: DEFAULT_IOS_ENABLE_PUSH_NOTIFICATIONS,
|
|
29
|
+
serverUpdateIntervalMinutes: DEFAULT_IOS_SERVER_UPDATE_INTERVAL_MINUTES,
|
|
30
|
+
serverUpdateRefresh: DEFAULT_IOS_SERVER_UPDATE_REFRESH,
|
|
31
|
+
userImagesPath: DEFAULT_IOS_USER_IMAGES_PATH,
|
|
32
|
+
widgetFamilies: DEFAULT_IOS_WIDGET_FAMILIES,
|
|
33
|
+
},
|
|
34
|
+
} as const
|