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,317 @@
|
|
|
1
|
+
import type { CLI_DEFAULTS } from './defaults'
|
|
2
|
+
|
|
3
|
+
export type VoltraPlatform = 'android' | 'ios'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-locale widget copy or build-time initial state paths.
|
|
7
|
+
* This intentionally matches the existing Expo plugin contract.
|
|
8
|
+
*/
|
|
9
|
+
export type WidgetLocalizedValue = Record<string, string>
|
|
10
|
+
|
|
11
|
+
/** Widget display text, either as a single string or localized by locale identifier. */
|
|
12
|
+
export type WidgetLabel = string | WidgetLocalizedValue
|
|
13
|
+
|
|
14
|
+
/** Path to a widget initial state module, either as a single path or localized by locale identifier. */
|
|
15
|
+
export type WidgetInitialStatePath = string | WidgetLocalizedValue
|
|
16
|
+
|
|
17
|
+
export interface AndroidWidgetServerUpdateConfig {
|
|
18
|
+
/** Server endpoint that returns widget state updates. */
|
|
19
|
+
url: string
|
|
20
|
+
/** Refresh interval, in minutes, for fetching server updates. */
|
|
21
|
+
intervalMinutes?: number
|
|
22
|
+
/** Whether fetched updates should trigger an immediate widget refresh. */
|
|
23
|
+
refresh?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AndroidWidgetAppIntentParameter {
|
|
27
|
+
/** Configuration key surfaced to env.configuration. */
|
|
28
|
+
name: string
|
|
29
|
+
/** Optional label for runtime configuration UIs. */
|
|
30
|
+
title?: string
|
|
31
|
+
/** Default value used before runtime configuration overrides it. */
|
|
32
|
+
default?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AndroidWidgetAppIntentConfig {
|
|
36
|
+
/** Parameters surfaced to env.configuration for Dynamic Widgets. */
|
|
37
|
+
parameters: AndroidWidgetAppIntentParameter[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AndroidWidgetConfig {
|
|
41
|
+
/** Stable widget identifier used in generated files and registrations. */
|
|
42
|
+
id: string
|
|
43
|
+
/** User-facing widget name shown by the launcher. */
|
|
44
|
+
displayName: WidgetLabel
|
|
45
|
+
/** User-facing widget description shown by the launcher. */
|
|
46
|
+
description: WidgetLabel
|
|
47
|
+
/** Minimum widget width in dp. */
|
|
48
|
+
minWidth?: number
|
|
49
|
+
/** Minimum widget height in dp. */
|
|
50
|
+
minHeight?: number
|
|
51
|
+
/** Minimum widget width in launcher grid cells. */
|
|
52
|
+
minCellWidth?: number
|
|
53
|
+
/** Minimum widget height in launcher grid cells. */
|
|
54
|
+
minCellHeight?: number
|
|
55
|
+
/** Default widget width in launcher grid cells. */
|
|
56
|
+
targetCellWidth: number
|
|
57
|
+
/** Default widget height in launcher grid cells. */
|
|
58
|
+
targetCellHeight: number
|
|
59
|
+
/** Supported resize directions for the Android widget. */
|
|
60
|
+
resizeMode?: 'none' | 'horizontal' | 'vertical' | 'horizontal|vertical'
|
|
61
|
+
/** Launcher surfaces where the widget can be placed. */
|
|
62
|
+
widgetCategory?: 'home_screen' | 'keyguard' | 'home_screen|keyguard'
|
|
63
|
+
/** Path to the build-time initial state module for this widget. */
|
|
64
|
+
initialStatePath?: WidgetInitialStatePath
|
|
65
|
+
/** Project-relative Dynamic Widget entry module. */
|
|
66
|
+
entry?: string
|
|
67
|
+
/** Server-driven update settings for this widget. */
|
|
68
|
+
serverUpdate?: AndroidWidgetServerUpdateConfig
|
|
69
|
+
/** Path to the preview image shown in widget pickers. */
|
|
70
|
+
previewImage?: string
|
|
71
|
+
/** Path to a preview layout XML file shown in widget pickers. */
|
|
72
|
+
previewLayout?: string
|
|
73
|
+
/** Dynamic Widget configuration parameters surfaced to env.configuration. */
|
|
74
|
+
appIntent?: AndroidWidgetAppIntentConfig
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type IOSWidgetFamily =
|
|
78
|
+
| 'systemSmall'
|
|
79
|
+
| 'systemMedium'
|
|
80
|
+
| 'systemLarge'
|
|
81
|
+
| 'systemExtraLarge'
|
|
82
|
+
| 'accessoryCircular'
|
|
83
|
+
| 'accessoryRectangular'
|
|
84
|
+
| 'accessoryInline'
|
|
85
|
+
|
|
86
|
+
export interface IOSWidgetServerUpdateConfig {
|
|
87
|
+
/** Server endpoint that returns widget state updates. */
|
|
88
|
+
url: string
|
|
89
|
+
/** Refresh interval, in minutes, for fetching server updates. */
|
|
90
|
+
intervalMinutes?: number
|
|
91
|
+
/** Whether fetched updates should trigger an immediate widget refresh. */
|
|
92
|
+
refresh?: boolean
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface IOSWidgetAppIntentParameter {
|
|
96
|
+
/** Configuration key surfaced to env.configuration. */
|
|
97
|
+
name: string
|
|
98
|
+
/** Label shown in the native Edit Widget sheet. */
|
|
99
|
+
title: string
|
|
100
|
+
/** Default value used before the user configures the widget. */
|
|
101
|
+
default?: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface IOSWidgetAppIntentConfig {
|
|
105
|
+
/** Parameters exposed through AppIntentConfiguration for Dynamic Widgets. */
|
|
106
|
+
parameters: IOSWidgetAppIntentParameter[]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface IOSWidgetConfig {
|
|
110
|
+
/** Stable widget identifier used in generated files and registrations. */
|
|
111
|
+
id: string
|
|
112
|
+
/** User-facing widget name shown in iOS widget configuration UI. */
|
|
113
|
+
displayName: WidgetLabel
|
|
114
|
+
/** User-facing widget description shown in iOS widget configuration UI. */
|
|
115
|
+
description: WidgetLabel
|
|
116
|
+
/** Supported iOS widget families for this widget. */
|
|
117
|
+
supportedFamilies?: IOSWidgetFamily[]
|
|
118
|
+
/** Path to the build-time initial state module for this widget. */
|
|
119
|
+
initialStatePath?: WidgetInitialStatePath
|
|
120
|
+
/** Project-relative Dynamic Widget entry module. */
|
|
121
|
+
entry?: string
|
|
122
|
+
/** Server-driven update settings for this widget. */
|
|
123
|
+
serverUpdate?: IOSWidgetServerUpdateConfig
|
|
124
|
+
/** Dynamic Widget AppIntent configuration. */
|
|
125
|
+
appIntent?: IOSWidgetAppIntentConfig
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface AndroidProjectOverrides {
|
|
129
|
+
/** Root directory of the Android native project. Defaults to `android/` under `projectRoot`. */
|
|
130
|
+
rootDir?: string
|
|
131
|
+
/** Android app module name. Defaults to `app` when it can be inferred. */
|
|
132
|
+
appModuleName?: string
|
|
133
|
+
/** Explicit path to the AndroidManifest.xml file for the app module. */
|
|
134
|
+
manifestPath?: string
|
|
135
|
+
/** Explicit Android package name if it cannot be derived from the project files. */
|
|
136
|
+
packageName?: string
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface IOSProjectOverrides {
|
|
140
|
+
/** Root directory of the iOS native project. Defaults to `ios/` under `projectRoot`. */
|
|
141
|
+
rootDir?: string
|
|
142
|
+
/** Explicit path to the `.xcodeproj` directory to use for discovery. */
|
|
143
|
+
xcodeprojPath?: string
|
|
144
|
+
/** Main application target name when the Xcode project has multiple app targets. */
|
|
145
|
+
mainTargetName?: string
|
|
146
|
+
/** Explicit path to the app target Info.plist file. */
|
|
147
|
+
infoPlistPath?: string
|
|
148
|
+
/** Explicit path to the main app entitlements file. */
|
|
149
|
+
entitlementsPath?: string
|
|
150
|
+
/** Explicit path to the Podfile. */
|
|
151
|
+
podfilePath?: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Public CLI config for Android. This stays close to the current Expo plugin props,
|
|
156
|
+
* with explicit project discovery overrides added for native projects.
|
|
157
|
+
*/
|
|
158
|
+
export interface VoltraAndroidConfig {
|
|
159
|
+
/** Whether to add the Android notification permission and related setup. */
|
|
160
|
+
enableNotifications?: boolean
|
|
161
|
+
/** Android widgets to generate and register. */
|
|
162
|
+
widgets?: AndroidWidgetConfig[]
|
|
163
|
+
/** Font files that should be bundled for Android widget rendering. */
|
|
164
|
+
fonts?: string[]
|
|
165
|
+
/** Directory containing user-provided images for Android widgets. */
|
|
166
|
+
userImagesPath?: string
|
|
167
|
+
/** Native Android project discovery overrides. */
|
|
168
|
+
project?: AndroidProjectOverrides
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Public CLI config for iOS. This stays close to the current Expo plugin props,
|
|
173
|
+
* with explicit project discovery overrides added for native projects.
|
|
174
|
+
*/
|
|
175
|
+
export interface VoltraIOSConfig {
|
|
176
|
+
/** Whether to enable push-notification-related iOS setup for widgets and Live Activities. */
|
|
177
|
+
enablePushNotifications?: boolean
|
|
178
|
+
/** App Group identifier used to share data between the app and widget extension. */
|
|
179
|
+
groupIdentifier?: string
|
|
180
|
+
/** iOS widgets to generate and register. */
|
|
181
|
+
widgets?: IOSWidgetConfig[]
|
|
182
|
+
/** Minimum iOS deployment target for generated widget targets. */
|
|
183
|
+
deploymentTarget?: string
|
|
184
|
+
/** Override for the generated widget extension target name. */
|
|
185
|
+
targetName?: string
|
|
186
|
+
/** Font files that should be bundled for iOS widget rendering. */
|
|
187
|
+
fonts?: string[]
|
|
188
|
+
/** Directory containing user-provided images for iOS widgets. */
|
|
189
|
+
userImagesPath?: string
|
|
190
|
+
/** Keychain access group shared by the app and extension. */
|
|
191
|
+
keychainGroup?: string
|
|
192
|
+
/** Native iOS project discovery overrides. */
|
|
193
|
+
project?: IOSProjectOverrides
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface VoltraConfig {
|
|
197
|
+
/** Root directory used to resolve relative paths in the Voltra config. Defaults to the config file directory. */
|
|
198
|
+
projectRoot?: string
|
|
199
|
+
/** Android-specific Voltra configuration. */
|
|
200
|
+
android?: VoltraAndroidConfig
|
|
201
|
+
/** iOS-specific Voltra configuration. */
|
|
202
|
+
ios?: VoltraIOSConfig
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface LoadedVoltraConfig {
|
|
206
|
+
/** Parsed Voltra config object loaded from disk. */
|
|
207
|
+
config: VoltraConfig
|
|
208
|
+
/** Absolute path to the loaded config file, when the config came from a file. */
|
|
209
|
+
configPath?: string
|
|
210
|
+
/** Directory that contained the loaded config source. */
|
|
211
|
+
configDir: string
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface NormalizedAndroidWidgetServerUpdateConfig {
|
|
215
|
+
/** Server endpoint that returns widget state updates. */
|
|
216
|
+
url: string
|
|
217
|
+
/** Refresh interval, in minutes, for fetching server updates. */
|
|
218
|
+
intervalMinutes: number
|
|
219
|
+
/** Whether fetched updates should trigger an immediate widget refresh. */
|
|
220
|
+
refresh: boolean
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface NormalizedAndroidWidgetConfig extends Omit<AndroidWidgetConfig, 'serverUpdate'> {
|
|
224
|
+
/** Server-driven update settings after defaults have been applied. */
|
|
225
|
+
serverUpdate?: NormalizedAndroidWidgetServerUpdateConfig
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface NormalizedIOSWidgetServerUpdateConfig {
|
|
229
|
+
/** Server endpoint that returns widget state updates. */
|
|
230
|
+
url: string
|
|
231
|
+
/** Refresh interval, in minutes, for fetching server updates. */
|
|
232
|
+
intervalMinutes: number
|
|
233
|
+
/** Whether fetched updates should trigger an immediate widget refresh. */
|
|
234
|
+
refresh: boolean
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface NormalizedIOSWidgetConfig extends Omit<IOSWidgetConfig, 'serverUpdate' | 'supportedFamilies'> {
|
|
238
|
+
/** Supported iOS widget families after defaults have been applied. */
|
|
239
|
+
supportedFamilies: IOSWidgetFamily[]
|
|
240
|
+
/** Server-driven update settings after defaults have been applied. */
|
|
241
|
+
serverUpdate?: NormalizedIOSWidgetServerUpdateConfig
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface NormalizedAndroidProjectConfig {
|
|
245
|
+
/** Absolute Android project root directory, if overridden. */
|
|
246
|
+
rootDir?: string
|
|
247
|
+
/** Resolved Android app module name. */
|
|
248
|
+
appModuleName?: string
|
|
249
|
+
/** Absolute path to AndroidManifest.xml, if overridden. */
|
|
250
|
+
manifestPath?: string
|
|
251
|
+
/** Explicit Android package name, if overridden. */
|
|
252
|
+
packageName?: string
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface NormalizedIOSProjectConfig {
|
|
256
|
+
/** Absolute iOS project root directory, if overridden. */
|
|
257
|
+
rootDir?: string
|
|
258
|
+
/** Absolute path to the `.xcodeproj` directory, if overridden. */
|
|
259
|
+
xcodeprojPath?: string
|
|
260
|
+
/** Explicit main iOS application target name, if overridden. */
|
|
261
|
+
mainTargetName?: string
|
|
262
|
+
/** Absolute path to the Info.plist file, if overridden. */
|
|
263
|
+
infoPlistPath?: string
|
|
264
|
+
/** Absolute path to the entitlements file, if overridden. */
|
|
265
|
+
entitlementsPath?: string
|
|
266
|
+
/** Absolute path to the Podfile, if overridden. */
|
|
267
|
+
podfilePath?: string
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface NormalizedVoltraAndroidConfig {
|
|
271
|
+
/** Whether Android notification setup should be applied. */
|
|
272
|
+
enableNotifications: boolean
|
|
273
|
+
/** Android widgets after validation and normalization. */
|
|
274
|
+
widgets: NormalizedAndroidWidgetConfig[]
|
|
275
|
+
/** Absolute font file paths for Android widgets. */
|
|
276
|
+
fonts: string[]
|
|
277
|
+
/** Absolute path to the Android user images directory. */
|
|
278
|
+
userImagesPath: string
|
|
279
|
+
/** Normalized Android native project discovery overrides. */
|
|
280
|
+
project: NormalizedAndroidProjectConfig
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface NormalizedVoltraIOSConfig {
|
|
284
|
+
/** Whether iOS push-notification-related setup should be applied. */
|
|
285
|
+
enablePushNotifications: boolean
|
|
286
|
+
/** App Group identifier used to share data between the app and extension. */
|
|
287
|
+
groupIdentifier?: string
|
|
288
|
+
/** iOS widgets after validation and normalization. */
|
|
289
|
+
widgets: NormalizedIOSWidgetConfig[]
|
|
290
|
+
/** Effective iOS deployment target for generated widget targets. */
|
|
291
|
+
deploymentTarget: string
|
|
292
|
+
/** Effective widget extension target name override, if provided. */
|
|
293
|
+
targetName?: string
|
|
294
|
+
/** Absolute font file paths for iOS widgets. */
|
|
295
|
+
fonts: string[]
|
|
296
|
+
/** Absolute path to the iOS user images directory. */
|
|
297
|
+
userImagesPath: string
|
|
298
|
+
/** Keychain access group shared by the app and extension. */
|
|
299
|
+
keychainGroup?: string
|
|
300
|
+
/** Normalized iOS native project discovery overrides. */
|
|
301
|
+
project: NormalizedIOSProjectConfig
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface NormalizedVoltraConfig {
|
|
305
|
+
/** Absolute path to the loaded config file, when the config came from a file. */
|
|
306
|
+
configPath?: string
|
|
307
|
+
/** Directory that contained the loaded config source. */
|
|
308
|
+
configDir: string
|
|
309
|
+
/** Absolute root directory used for resolving all project-relative paths. */
|
|
310
|
+
projectRoot: string
|
|
311
|
+
/** Normalized Android-specific Voltra configuration. */
|
|
312
|
+
android?: NormalizedVoltraAndroidConfig
|
|
313
|
+
/** Normalized iOS-specific Voltra configuration. */
|
|
314
|
+
ios?: NormalizedVoltraIOSConfig
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export type CliDefaults = typeof CLI_DEFAULTS
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare module 'cosmiconfig' {
|
|
2
|
+
export interface CosmiconfigResult {
|
|
3
|
+
config: unknown
|
|
4
|
+
filepath: string
|
|
5
|
+
isEmpty?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CosmiconfigExplorer {
|
|
9
|
+
search(searchFrom?: string): Promise<CosmiconfigResult | null>
|
|
10
|
+
load(filepath: string): Promise<CosmiconfigResult | null>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CosmiconfigOptions {
|
|
14
|
+
searchPlaces?: string[]
|
|
15
|
+
loaders?: Record<string, unknown>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const defaultLoaders: Record<string, unknown>
|
|
19
|
+
|
|
20
|
+
export function cosmiconfig(moduleName: string, options?: CosmiconfigOptions): CosmiconfigExplorer
|
|
21
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
|
|
4
|
+
import type { VoltraPlatform } from '../config/types'
|
|
5
|
+
|
|
6
|
+
const PLATFORM_PACKAGE_NAMES: Record<VoltraPlatform, string> = {
|
|
7
|
+
android: '@use-voltra/android',
|
|
8
|
+
ios: '@use-voltra/ios',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const PLATFORM_CLIENT_PACKAGE_NAMES: Record<VoltraPlatform, string> = {
|
|
12
|
+
android: '@use-voltra/android-client',
|
|
13
|
+
ios: '@use-voltra/ios-client',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getPlatformPackageName(platform: VoltraPlatform): string {
|
|
17
|
+
return PLATFORM_PACKAGE_NAMES[platform]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getPlatformClientPackageName(platform: VoltraPlatform): string {
|
|
21
|
+
return PLATFORM_CLIENT_PACKAGE_NAMES[platform]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isPlatformPackageInstalled(projectRoot: string, platform: VoltraPlatform): boolean {
|
|
25
|
+
return getMissingPlatformPackages(projectRoot, platform).length === 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getMissingPlatformPackages(projectRoot: string, platform: VoltraPlatform): string[] {
|
|
29
|
+
const projectRequire = createProjectRequire(projectRoot)
|
|
30
|
+
const packageNames = getRequiredPlatformPackageNames(platform)
|
|
31
|
+
|
|
32
|
+
return packageNames.filter((packageName) => !canResolvePackage(projectRequire, packageName))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function requirePlatformPackage<TPackage>(projectRoot: string, platform: VoltraPlatform): TPackage {
|
|
36
|
+
return createProjectRequire(projectRoot)(getPlatformPackageName(platform)) as TPackage
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getMissingPlatformPackageMessage(
|
|
40
|
+
platform: VoltraPlatform,
|
|
41
|
+
packageNames = getRequiredPlatformPackageNames(platform)
|
|
42
|
+
): string {
|
|
43
|
+
const packageLabel = packageNames.length === 1 ? 'package' : 'packages'
|
|
44
|
+
const verb = packageNames.length === 1 ? 'is' : 'are'
|
|
45
|
+
const pronoun = packageNames.length === 1 ? 'it' : 'them'
|
|
46
|
+
|
|
47
|
+
return `Required ${packageLabel} ${formatPackageList(
|
|
48
|
+
packageNames
|
|
49
|
+
)} ${verb} not installed in the app project. Install ${pronoun} because voltra.config includes a ${platform} config block.`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getRequiredPlatformPackageNames(platform: VoltraPlatform): string[] {
|
|
53
|
+
return [getPlatformPackageName(platform), getPlatformClientPackageName(platform)]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function canResolvePackage(projectRequire: NodeRequire, packageName: string): boolean {
|
|
57
|
+
try {
|
|
58
|
+
projectRequire.resolve(`${packageName}/package.json`)
|
|
59
|
+
return true
|
|
60
|
+
} catch {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatPackageList(packageNames: string[]): string {
|
|
66
|
+
if (packageNames.length === 1) {
|
|
67
|
+
return packageNames[0]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return `${packageNames.slice(0, -1).join(', ')} and ${packageNames[packageNames.length - 1]}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createProjectRequire(projectRoot: string): NodeRequire {
|
|
74
|
+
return createRequire(path.join(projectRoot, 'package.json'))
|
|
75
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { Stats } from 'node:fs'
|
|
5
|
+
|
|
6
|
+
import { VoltraCliError } from '../reporting/summary'
|
|
7
|
+
|
|
8
|
+
import type { NormalizedAndroidProjectConfig } from '../config/types'
|
|
9
|
+
|
|
10
|
+
const ANDROID_MANIFEST_FILE_NAME = 'AndroidManifest.xml'
|
|
11
|
+
|
|
12
|
+
export interface AndroidProjectDiscovery {
|
|
13
|
+
androidRoot: string
|
|
14
|
+
appModuleName: string
|
|
15
|
+
appModuleRoot: string
|
|
16
|
+
manifestPath: string
|
|
17
|
+
buildGradlePath: string
|
|
18
|
+
packageName: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class AndroidProjectDiscoveryError extends VoltraCliError {
|
|
22
|
+
constructor(message: string) {
|
|
23
|
+
super(message, 'VOLTRA_ANDROID_DISCOVERY_FAILED')
|
|
24
|
+
this.name = 'AndroidProjectDiscoveryError'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function discoverAndroidProject(
|
|
29
|
+
projectRoot: string,
|
|
30
|
+
config: NormalizedAndroidProjectConfig
|
|
31
|
+
): Promise<AndroidProjectDiscovery> {
|
|
32
|
+
const androidRoot = await resolveAndroidRoot(projectRoot, config)
|
|
33
|
+
const manifestPath = await resolveManifestPath(androidRoot, config)
|
|
34
|
+
const appModuleName = resolveAppModuleName(androidRoot, manifestPath, config.appModuleName)
|
|
35
|
+
const appModuleRoot = path.join(androidRoot, appModuleName)
|
|
36
|
+
|
|
37
|
+
await ensureDirectory(appModuleRoot, `Android app module directory does not exist: ${appModuleRoot}`)
|
|
38
|
+
ensureManifestBelongsToAppModule(appModuleRoot, manifestPath)
|
|
39
|
+
|
|
40
|
+
const buildGradlePath = await resolveBuildGradlePath(appModuleRoot)
|
|
41
|
+
const packageName = config.packageName ?? (await resolvePackageName(buildGradlePath, manifestPath))
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
androidRoot,
|
|
45
|
+
appModuleName,
|
|
46
|
+
appModuleRoot,
|
|
47
|
+
manifestPath,
|
|
48
|
+
buildGradlePath,
|
|
49
|
+
packageName,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function resolveAndroidRoot(projectRoot: string, config: NormalizedAndroidProjectConfig): Promise<string> {
|
|
54
|
+
const androidRoot = config.rootDir ?? path.join(projectRoot, 'android')
|
|
55
|
+
|
|
56
|
+
await ensureDirectory(
|
|
57
|
+
androidRoot,
|
|
58
|
+
config.rootDir
|
|
59
|
+
? `Configured Android root directory does not exist: ${androidRoot}`
|
|
60
|
+
: `Android root directory does not exist at ${androidRoot}. Set android.project.rootDir to override the default android/ layout.`
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return androidRoot
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function resolveManifestPath(androidRoot: string, config: NormalizedAndroidProjectConfig): Promise<string> {
|
|
67
|
+
const manifestPath =
|
|
68
|
+
config.manifestPath ??
|
|
69
|
+
path.join(androidRoot, config.appModuleName ?? 'app', 'src', 'main', ANDROID_MANIFEST_FILE_NAME)
|
|
70
|
+
|
|
71
|
+
await ensureFile(
|
|
72
|
+
manifestPath,
|
|
73
|
+
config.manifestPath
|
|
74
|
+
? `Configured Android manifest does not exist: ${manifestPath}`
|
|
75
|
+
: `Android manifest does not exist at ${manifestPath}. Set android.project.appModuleName or android.project.manifestPath to override the default app/src/main/AndroidManifest.xml layout.`
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return manifestPath
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveAppModuleName(
|
|
82
|
+
androidRoot: string,
|
|
83
|
+
manifestPath: string,
|
|
84
|
+
configuredAppModuleName: string | undefined
|
|
85
|
+
): string {
|
|
86
|
+
if (configuredAppModuleName) {
|
|
87
|
+
return configuredAppModuleName
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const relativeManifestPath = path.relative(androidRoot, manifestPath)
|
|
91
|
+
|
|
92
|
+
if (relativeManifestPath.startsWith('..') || path.isAbsolute(relativeManifestPath)) {
|
|
93
|
+
throw new AndroidProjectDiscoveryError(
|
|
94
|
+
`Android manifest ${manifestPath} is outside Android root ${androidRoot}. Set android.project.appModuleName to identify the app module explicitly.`
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const segments = relativeManifestPath.split(path.sep)
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
segments.length >= 4 &&
|
|
102
|
+
segments[1] === 'src' &&
|
|
103
|
+
segments[2] === 'main' &&
|
|
104
|
+
segments[3] === ANDROID_MANIFEST_FILE_NAME
|
|
105
|
+
) {
|
|
106
|
+
return segments[0]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
throw new AndroidProjectDiscoveryError(
|
|
110
|
+
`Could not derive Android app module from manifest path ${manifestPath}. Set android.project.appModuleName explicitly.`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function resolveBuildGradlePath(appModuleRoot: string): Promise<string> {
|
|
115
|
+
const buildGradlePath = path.join(appModuleRoot, 'build.gradle')
|
|
116
|
+
const buildGradleKtsPath = path.join(appModuleRoot, 'build.gradle.kts')
|
|
117
|
+
const hasBuildGradle = await pathExists(buildGradlePath)
|
|
118
|
+
const hasBuildGradleKts = await pathExists(buildGradleKtsPath)
|
|
119
|
+
|
|
120
|
+
if (hasBuildGradle && hasBuildGradleKts) {
|
|
121
|
+
throw new AndroidProjectDiscoveryError(
|
|
122
|
+
`Android app module has both build.gradle and build.gradle.kts: ${appModuleRoot}. Remove the ambiguity before running voltra apply.`
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (hasBuildGradle) {
|
|
127
|
+
return buildGradlePath
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (hasBuildGradleKts) {
|
|
131
|
+
return buildGradleKtsPath
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw new AndroidProjectDiscoveryError(
|
|
135
|
+
`Android app module build file does not exist in ${appModuleRoot}. Expected build.gradle or build.gradle.kts.`
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function ensureManifestBelongsToAppModule(appModuleRoot: string, manifestPath: string): void {
|
|
140
|
+
const relativeManifestPath = path.relative(appModuleRoot, manifestPath)
|
|
141
|
+
|
|
142
|
+
if (relativeManifestPath.startsWith('..') || path.isAbsolute(relativeManifestPath)) {
|
|
143
|
+
throw new AndroidProjectDiscoveryError(
|
|
144
|
+
`Android manifest ${manifestPath} is outside app module ${appModuleRoot}. Align android.project.appModuleName and android.project.manifestPath so they point at the same module.`
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function resolvePackageName(buildGradlePath: string, manifestPath: string): Promise<string> {
|
|
150
|
+
const buildGradle = stripGradleComments(await fs.readFile(buildGradlePath, 'utf8'))
|
|
151
|
+
const namespace = matchGradleStringLiteral(buildGradle, 'namespace')
|
|
152
|
+
|
|
153
|
+
if (namespace) {
|
|
154
|
+
return namespace
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const applicationId = matchGradleStringLiteral(buildGradle, 'applicationId')
|
|
158
|
+
|
|
159
|
+
if (applicationId) {
|
|
160
|
+
return applicationId
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const manifest = await fs.readFile(manifestPath, 'utf8')
|
|
164
|
+
const manifestPackage = matchManifestPackage(manifest)
|
|
165
|
+
|
|
166
|
+
if (manifestPackage) {
|
|
167
|
+
return manifestPackage
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
throw new AndroidProjectDiscoveryError(
|
|
171
|
+
`Could not determine Android package name from ${buildGradlePath} or ${manifestPath}. Set android.project.packageName explicitly or add namespace/applicationId to the app module build file.`
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function matchGradleStringLiteral(content: string, propertyName: string): string | undefined {
|
|
176
|
+
const match = content.match(new RegExp(`\\b${propertyName}\\s*(?:=)?\\s*['"]([^'"]+)['"]`))
|
|
177
|
+
|
|
178
|
+
return match?.[1]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function stripGradleComments(content: string): string {
|
|
182
|
+
return content.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '$1')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function matchManifestPackage(content: string): string | undefined {
|
|
186
|
+
const match = content.match(/<manifest\b[^>]*\bpackage\s*=\s*['"]([^'"]+)['"]/)
|
|
187
|
+
|
|
188
|
+
return match?.[1]
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function ensureDirectory(dirPath: string, message: string): Promise<void> {
|
|
192
|
+
const stat = await readPathStat(dirPath)
|
|
193
|
+
|
|
194
|
+
if (!stat) {
|
|
195
|
+
throw new AndroidProjectDiscoveryError(message)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!stat.isDirectory()) {
|
|
199
|
+
throw new AndroidProjectDiscoveryError(`Expected a directory but found a file: ${dirPath}`)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function ensureFile(filePath: string, message: string): Promise<void> {
|
|
204
|
+
const stat = await readPathStat(filePath)
|
|
205
|
+
|
|
206
|
+
if (!stat) {
|
|
207
|
+
throw new AndroidProjectDiscoveryError(message)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!stat.isFile()) {
|
|
211
|
+
throw new AndroidProjectDiscoveryError(`Expected a file but found a directory: ${filePath}`)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function pathExists(targetPath: string): Promise<boolean> {
|
|
216
|
+
return (await readPathStat(targetPath)) !== undefined
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function readPathStat(targetPath: string): Promise<Stats | undefined> {
|
|
220
|
+
try {
|
|
221
|
+
return await fs.stat(targetPath)
|
|
222
|
+
} catch (error: unknown) {
|
|
223
|
+
if (isNotFoundError(error)) {
|
|
224
|
+
return undefined
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
throw error
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isNotFoundError(error: unknown): error is NodeJS.ErrnoException {
|
|
232
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
|
|
233
|
+
}
|