voltra 2.2.0 → 2.3.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.
- package/build/cjs/apply/index.js +6 -1
- package/build/cjs/apply/index.js.map +1 -1
- package/build/cjs/config/normalize.js +150 -31
- package/build/cjs/config/normalize.js.map +1 -1
- package/build/cjs/config/perConfiguration.js +29 -0
- package/build/cjs/config/perConfiguration.js.map +1 -0
- package/build/cjs/config/serverUpdate.js +119 -0
- package/build/cjs/config/serverUpdate.js.map +1 -0
- package/build/cjs/config/widgetKind.js +25 -0
- package/build/cjs/config/widgetKind.js.map +1 -0
- package/build/cjs/dependencies/platformPackages.js +9 -0
- package/build/cjs/dependencies/platformPackages.js.map +1 -1
- package/build/cjs/discovery/ios.js +50 -31
- package/build/cjs/discovery/ios.js.map +1 -1
- package/build/cjs/discovery/metro.js +52 -0
- package/build/cjs/discovery/metro.js.map +1 -0
- package/build/cjs/index.js +6 -2
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/platforms/android/generated.js +98 -40
- package/build/cjs/platforms/android/generated.js.map +1 -1
- package/build/cjs/platforms/android/widgetSizing.js +89 -0
- package/build/cjs/platforms/android/widgetSizing.js.map +1 -0
- package/build/cjs/platforms/ios/apply.js +21 -11
- package/build/cjs/platforms/ios/apply.js.map +1 -1
- package/build/cjs/platforms/ios/buildConfigurationValues.js +73 -0
- package/build/cjs/platforms/ios/buildConfigurationValues.js.map +1 -0
- package/build/cjs/platforms/ios/entitlements.js +34 -19
- package/build/cjs/platforms/ios/entitlements.js.map +1 -1
- package/build/cjs/platforms/ios/generated.js +79 -32
- package/build/cjs/platforms/ios/generated.js.map +1 -1
- package/build/cjs/platforms/ios/plist.js +22 -4
- package/build/cjs/platforms/ios/plist.js.map +1 -1
- package/build/cjs/platforms/ios/targetName.js.map +1 -1
- package/build/cjs/platforms/ios/xcodeTarget.js +166 -23
- package/build/cjs/platforms/ios/xcodeTarget.js.map +1 -1
- package/build/cjs/platforms/shared/widgetModule.js +18 -145
- package/build/cjs/platforms/shared/widgetModule.js.map +1 -1
- package/build/esm/apply/index.js +6 -1
- package/build/esm/apply/index.js.map +1 -1
- package/build/esm/config/normalize.js +150 -31
- package/build/esm/config/normalize.js.map +1 -1
- package/build/esm/config/perConfiguration.js +25 -0
- package/build/esm/config/perConfiguration.js.map +1 -0
- package/build/esm/config/serverUpdate.js +112 -0
- package/build/esm/config/serverUpdate.js.map +1 -0
- package/build/esm/config/widgetKind.js +20 -0
- package/build/esm/config/widgetKind.js.map +1 -0
- package/build/esm/dependencies/platformPackages.js +8 -0
- package/build/esm/dependencies/platformPackages.js.map +1 -1
- package/build/esm/discovery/ios.js +50 -31
- package/build/esm/discovery/ios.js.map +1 -1
- package/build/esm/discovery/metro.js +46 -0
- package/build/esm/discovery/metro.js.map +1 -0
- package/build/esm/index.js +1 -0
- package/build/esm/index.js.map +1 -1
- package/build/esm/platforms/android/generated.js +100 -42
- package/build/esm/platforms/android/generated.js.map +1 -1
- package/build/esm/platforms/android/widgetSizing.js +85 -0
- package/build/esm/platforms/android/widgetSizing.js.map +1 -0
- package/build/esm/platforms/ios/apply.js +21 -11
- package/build/esm/platforms/ios/apply.js.map +1 -1
- package/build/esm/platforms/ios/buildConfigurationValues.js +68 -0
- package/build/esm/platforms/ios/buildConfigurationValues.js.map +1 -0
- package/build/esm/platforms/ios/entitlements.js +34 -19
- package/build/esm/platforms/ios/entitlements.js.map +1 -1
- package/build/esm/platforms/ios/generated.js +80 -33
- package/build/esm/platforms/ios/generated.js.map +1 -1
- package/build/esm/platforms/ios/plist.js +22 -4
- package/build/esm/platforms/ios/plist.js.map +1 -1
- package/build/esm/platforms/ios/targetName.js.map +1 -1
- package/build/esm/platforms/ios/xcodeTarget.js +166 -23
- package/build/esm/platforms/ios/xcodeTarget.js.map +1 -1
- package/build/esm/platforms/shared/widgetModule.js +16 -107
- package/build/esm/platforms/shared/widgetModule.js.map +1 -1
- package/build/types/apply/index.d.ts.map +1 -1
- package/build/types/config/normalize.d.ts.map +1 -1
- package/build/types/config/perConfiguration.d.ts +18 -0
- package/build/types/config/perConfiguration.d.ts.map +1 -0
- package/build/types/config/serverUpdate.d.ts +80 -0
- package/build/types/config/serverUpdate.d.ts.map +1 -0
- package/build/types/config/types.d.ts +91 -30
- package/build/types/config/types.d.ts.map +1 -1
- package/build/types/config/widgetKind.d.ts +11 -0
- package/build/types/config/widgetKind.d.ts.map +1 -0
- package/build/types/dependencies/platformPackages.d.ts +1 -0
- package/build/types/dependencies/platformPackages.d.ts.map +1 -1
- package/build/types/discovery/ios.d.ts +10 -0
- package/build/types/discovery/ios.d.ts.map +1 -1
- package/build/types/discovery/metro.d.ts +21 -0
- package/build/types/discovery/metro.d.ts.map +1 -0
- package/build/types/index.d.ts +3 -1
- package/build/types/index.d.ts.map +1 -1
- package/build/types/platforms/android/generated.d.ts.map +1 -1
- package/build/types/platforms/android/widgetSizing.d.ts +50 -0
- package/build/types/platforms/android/widgetSizing.d.ts.map +1 -0
- package/build/types/platforms/ios/apply.d.ts.map +1 -1
- package/build/types/platforms/ios/buildConfigurationValues.d.ts +33 -0
- package/build/types/platforms/ios/buildConfigurationValues.d.ts.map +1 -0
- package/build/types/platforms/ios/entitlements.d.ts +4 -4
- package/build/types/platforms/ios/entitlements.d.ts.map +1 -1
- package/build/types/platforms/ios/generated.d.ts +14 -2
- package/build/types/platforms/ios/generated.d.ts.map +1 -1
- package/build/types/platforms/ios/plist.d.ts +3 -3
- package/build/types/platforms/ios/plist.d.ts.map +1 -1
- package/build/types/platforms/ios/podfile.d.ts +2 -2
- package/build/types/platforms/ios/podfile.d.ts.map +1 -1
- package/build/types/platforms/ios/targetName.d.ts +2 -2
- package/build/types/platforms/ios/targetName.d.ts.map +1 -1
- package/build/types/platforms/ios/xcodeTarget.d.ts +4 -2
- package/build/types/platforms/ios/xcodeTarget.d.ts.map +1 -1
- package/build/types/platforms/shared/widgetModule.d.ts +12 -3
- package/build/types/platforms/shared/widgetModule.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/apply/index.ts +6 -1
- package/src/config/normalize.node.test.ts +105 -0
- package/src/config/normalize.ts +217 -45
- package/src/config/perConfiguration.ts +49 -0
- package/src/config/serverUpdate.ts +160 -0
- package/src/config/types.ts +97 -30
- package/src/config/widgetKind.node.test.ts +27 -0
- package/src/config/widgetKind.ts +27 -0
- package/src/dependencies/platformPackages.ts +11 -0
- package/src/discovery/ios.ts +104 -60
- package/src/discovery/metro.node.test.ts +91 -0
- package/src/discovery/metro.ts +54 -0
- package/src/index.ts +8 -0
- package/src/platforms/android/generated.ts +144 -47
- package/src/platforms/android/widgetSizing.ts +112 -0
- package/src/platforms/ios/apply.ts +21 -11
- package/src/platforms/ios/buildConfigurationValues.ts +105 -0
- package/src/platforms/ios/entitlements.ts +55 -31
- package/src/platforms/ios/generated.node.test.ts +61 -0
- package/src/platforms/ios/generated.ts +137 -38
- package/src/platforms/ios/plist.ts +36 -7
- package/src/platforms/ios/podfile.ts +2 -2
- package/src/platforms/ios/targetName.ts +2 -2
- package/src/platforms/ios/xcodeTarget.ts +248 -43
- package/src/platforms/shared/widgetModule.ts +22 -140
package/src/config/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CLI_DEFAULTS } from './defaults'
|
|
2
|
+
import type { PerConfiguration } from './perConfiguration'
|
|
2
3
|
|
|
3
4
|
export type VoltraPlatform = 'android' | 'ios'
|
|
4
5
|
|
|
@@ -15,8 +16,11 @@ export type WidgetLabel = string | WidgetLocalizedValue
|
|
|
15
16
|
export type WidgetInitialStatePath = string | WidgetLocalizedValue
|
|
16
17
|
|
|
17
18
|
export interface AndroidWidgetServerUpdateConfig {
|
|
18
|
-
/**
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Server endpoint that returns widget state updates. Optional — omit it to mark the widget
|
|
21
|
+
* server-driven and supply the URL at runtime with `setWidgetServerUpdate`.
|
|
22
|
+
*/
|
|
23
|
+
url?: string
|
|
20
24
|
/** Refresh interval, in minutes, for fetching server updates. */
|
|
21
25
|
intervalMinutes?: number
|
|
22
26
|
/** Whether fetched updates should trigger an immediate widget refresh. */
|
|
@@ -44,14 +48,32 @@ export interface AndroidWidgetConfig {
|
|
|
44
48
|
displayName: WidgetLabel
|
|
45
49
|
/** User-facing widget description shown by the launcher. */
|
|
46
50
|
description: WidgetLabel
|
|
47
|
-
/** Minimum widget width in dp. */
|
|
51
|
+
/** Minimum widget width in dp. Only affects Android 11 and older. */
|
|
48
52
|
minWidth?: number
|
|
49
|
-
/** Minimum widget height in dp. */
|
|
53
|
+
/** Minimum widget height in dp. Only affects Android 11 and older. */
|
|
50
54
|
minHeight?: number
|
|
51
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Minimum widget width in launcher grid cells.
|
|
57
|
+
*
|
|
58
|
+
* @deprecated Use `minWidth` instead. The value is approximated in dp for Android 11 and
|
|
59
|
+
* older.
|
|
60
|
+
*/
|
|
52
61
|
minCellWidth?: number
|
|
53
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Minimum widget height in launcher grid cells.
|
|
64
|
+
*
|
|
65
|
+
* @deprecated Use `minHeight` instead. The value is approximated in dp for Android 11 and
|
|
66
|
+
* older.
|
|
67
|
+
*/
|
|
54
68
|
minCellHeight?: number
|
|
69
|
+
/** Minimum width, in dp, the widget can be resized down to. */
|
|
70
|
+
minResizeWidth?: number
|
|
71
|
+
/** Minimum height, in dp, the widget can be resized down to. */
|
|
72
|
+
minResizeHeight?: number
|
|
73
|
+
/** Maximum width, in dp, the widget can be resized up to. Honoured on Android 12 and newer. */
|
|
74
|
+
maxResizeWidth?: number
|
|
75
|
+
/** Maximum height, in dp, the widget can be resized up to. Honoured on Android 12 and newer. */
|
|
76
|
+
maxResizeHeight?: number
|
|
55
77
|
/** Default widget width in launcher grid cells. */
|
|
56
78
|
targetCellWidth: number
|
|
57
79
|
/** Default widget height in launcher grid cells. */
|
|
@@ -84,8 +106,11 @@ export type IOSWidgetFamily =
|
|
|
84
106
|
| 'accessoryInline'
|
|
85
107
|
|
|
86
108
|
export interface IOSWidgetServerUpdateConfig {
|
|
87
|
-
/**
|
|
88
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Server endpoint that returns widget state updates. Optional — omit it to mark the widget
|
|
111
|
+
* server-driven and supply the URL at runtime with `setWidgetServerUpdate`.
|
|
112
|
+
*/
|
|
113
|
+
url?: string
|
|
89
114
|
/** Refresh interval, in minutes, for fetching server updates. */
|
|
90
115
|
intervalMinutes?: number
|
|
91
116
|
/** Whether fetched updates should trigger an immediate widget refresh. */
|
|
@@ -109,6 +134,12 @@ export interface IOSWidgetAppIntentConfig {
|
|
|
109
134
|
export interface IOSWidgetConfig {
|
|
110
135
|
/** Stable widget identifier used in generated files and registrations. */
|
|
111
136
|
id: string
|
|
137
|
+
/**
|
|
138
|
+
* WidgetKit `kind` of the generated widget. Defaults to `Voltra_Widget_<id>`.
|
|
139
|
+
* Pin it to the kind of a pre-Voltra widget so already placed instances survive the migration
|
|
140
|
+
* (WidgetKit identifies a placed widget by extension bundle id + kind).
|
|
141
|
+
*/
|
|
142
|
+
kind?: string
|
|
112
143
|
/** User-facing widget name shown in iOS widget configuration UI. */
|
|
113
144
|
displayName: WidgetLabel
|
|
114
145
|
/** User-facing widget description shown in iOS widget configuration UI. */
|
|
@@ -145,8 +176,11 @@ export interface IOSProjectOverrides {
|
|
|
145
176
|
mainTargetName?: string
|
|
146
177
|
/** Explicit path to the app target Info.plist file. */
|
|
147
178
|
infoPlistPath?: string
|
|
148
|
-
/**
|
|
149
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Explicit path to the main app entitlements file, or one path per Xcode build configuration
|
|
181
|
+
* name when each environment has its own.
|
|
182
|
+
*/
|
|
183
|
+
entitlementsPath?: PerConfiguration<string>
|
|
150
184
|
/** Explicit path to the Podfile. */
|
|
151
185
|
podfilePath?: string
|
|
152
186
|
}
|
|
@@ -175,8 +209,11 @@ export interface VoltraAndroidConfig {
|
|
|
175
209
|
export interface VoltraIOSConfig {
|
|
176
210
|
/** Whether to enable push-notification-related iOS setup for widgets and Live Activities. */
|
|
177
211
|
enablePushNotifications?: boolean
|
|
178
|
-
/**
|
|
179
|
-
|
|
212
|
+
/**
|
|
213
|
+
* App Group identifier used to share data between the app and widget extension, or one per Xcode
|
|
214
|
+
* build configuration name when each environment has its own.
|
|
215
|
+
*/
|
|
216
|
+
groupIdentifier?: PerConfiguration<string>
|
|
180
217
|
/** iOS widgets to generate and register. */
|
|
181
218
|
widgets?: IOSWidgetConfig[]
|
|
182
219
|
/** Minimum iOS deployment target for generated widget targets. */
|
|
@@ -187,8 +224,11 @@ export interface VoltraIOSConfig {
|
|
|
187
224
|
fonts?: string[]
|
|
188
225
|
/** Directory containing user-provided images for iOS widgets. */
|
|
189
226
|
userImagesPath?: string
|
|
190
|
-
/**
|
|
191
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Keychain access group shared by the app and extension, or one per Xcode build configuration
|
|
229
|
+
* name when each environment has its own.
|
|
230
|
+
*/
|
|
231
|
+
keychainGroup?: PerConfiguration<string>
|
|
192
232
|
/** Native iOS project discovery overrides. */
|
|
193
233
|
project?: IOSProjectOverrides
|
|
194
234
|
}
|
|
@@ -211,28 +251,27 @@ export interface LoadedVoltraConfig {
|
|
|
211
251
|
configDir: string
|
|
212
252
|
}
|
|
213
253
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Build-time server-update defaults after normalization. The device treats these as the lowest
|
|
256
|
+
* settings layer; `setWidgetServerUpdate` overrides `url` and `intervalMinutes` at runtime.
|
|
257
|
+
*/
|
|
258
|
+
export interface NormalizedWidgetServerUpdateConfig {
|
|
259
|
+
/** Server endpoint, when app.json set one. Absent means "URL supplied at runtime". */
|
|
260
|
+
url?: string
|
|
217
261
|
/** Refresh interval, in minutes, for fetching server updates. */
|
|
218
262
|
intervalMinutes: number
|
|
219
|
-
/** Whether
|
|
263
|
+
/** Whether the widget draws a refresh button. Build-time only: it is generated UI structure. */
|
|
220
264
|
refresh: boolean
|
|
221
265
|
}
|
|
222
266
|
|
|
267
|
+
export type NormalizedAndroidWidgetServerUpdateConfig = NormalizedWidgetServerUpdateConfig
|
|
268
|
+
|
|
223
269
|
export interface NormalizedAndroidWidgetConfig extends Omit<AndroidWidgetConfig, 'serverUpdate'> {
|
|
224
270
|
/** Server-driven update settings after defaults have been applied. */
|
|
225
271
|
serverUpdate?: NormalizedAndroidWidgetServerUpdateConfig
|
|
226
272
|
}
|
|
227
273
|
|
|
228
|
-
export
|
|
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
|
-
}
|
|
274
|
+
export type NormalizedIOSWidgetServerUpdateConfig = NormalizedWidgetServerUpdateConfig
|
|
236
275
|
|
|
237
276
|
export interface NormalizedIOSWidgetConfig extends Omit<IOSWidgetConfig, 'serverUpdate' | 'supportedFamilies'> {
|
|
238
277
|
/** Supported iOS widget families after defaults have been applied. */
|
|
@@ -261,8 +300,8 @@ export interface NormalizedIOSProjectConfig {
|
|
|
261
300
|
mainTargetName?: string
|
|
262
301
|
/** Absolute path to the Info.plist file, if overridden. */
|
|
263
302
|
infoPlistPath?: string
|
|
264
|
-
/** Absolute path to the entitlements file, if overridden. */
|
|
265
|
-
entitlementsPath?: string
|
|
303
|
+
/** Absolute path to the entitlements file, or one per build configuration name, if overridden. */
|
|
304
|
+
entitlementsPath?: PerConfiguration<string>
|
|
266
305
|
/** Absolute path to the Podfile, if overridden. */
|
|
267
306
|
podfilePath?: string
|
|
268
307
|
}
|
|
@@ -284,7 +323,7 @@ export interface NormalizedVoltraIOSConfig {
|
|
|
284
323
|
/** Whether iOS push-notification-related setup should be applied. */
|
|
285
324
|
enablePushNotifications: boolean
|
|
286
325
|
/** App Group identifier used to share data between the app and extension. */
|
|
287
|
-
groupIdentifier?: string
|
|
326
|
+
groupIdentifier?: PerConfiguration<string>
|
|
288
327
|
/** iOS widgets after validation and normalization. */
|
|
289
328
|
widgets: NormalizedIOSWidgetConfig[]
|
|
290
329
|
/** Effective iOS deployment target for generated widget targets. */
|
|
@@ -296,11 +335,37 @@ export interface NormalizedVoltraIOSConfig {
|
|
|
296
335
|
/** Absolute path to the iOS user images directory. */
|
|
297
336
|
userImagesPath: string
|
|
298
337
|
/** Keychain access group shared by the app and extension. */
|
|
299
|
-
keychainGroup?: string
|
|
338
|
+
keychainGroup?: PerConfiguration<string>
|
|
300
339
|
/** Normalized iOS native project discovery overrides. */
|
|
301
340
|
project: NormalizedIOSProjectConfig
|
|
302
341
|
}
|
|
303
342
|
|
|
343
|
+
/**
|
|
344
|
+
* iOS project overrides after per-build-configuration values have been resolved against the Xcode
|
|
345
|
+
* project.
|
|
346
|
+
*/
|
|
347
|
+
export type ResolvedIOSProjectConfig = Omit<NormalizedIOSProjectConfig, 'entitlementsPath'> & {
|
|
348
|
+
/** Absolute path to the entitlements file of the default build configuration, if overridden. */
|
|
349
|
+
entitlementsPath?: string
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* iOS config as the platform mutators consume it: every per-build-configuration value has been
|
|
354
|
+
* collapsed to a single string, either the configured value or a reference to a build setting
|
|
355
|
+
* Voltra writes per build configuration.
|
|
356
|
+
*/
|
|
357
|
+
export type ResolvedVoltraIOSConfig = Omit<
|
|
358
|
+
NormalizedVoltraIOSConfig,
|
|
359
|
+
'groupIdentifier' | 'keychainGroup' | 'project'
|
|
360
|
+
> & {
|
|
361
|
+
/** App Group identifier used to share data between the app and extension. */
|
|
362
|
+
groupIdentifier?: string
|
|
363
|
+
/** Keychain access group shared by the app and extension. */
|
|
364
|
+
keychainGroup?: string
|
|
365
|
+
/** Resolved iOS native project discovery overrides. */
|
|
366
|
+
project: ResolvedIOSProjectConfig
|
|
367
|
+
}
|
|
368
|
+
|
|
304
369
|
export interface NormalizedVoltraConfig {
|
|
305
370
|
/** Absolute path to the loaded config file, when the config came from a file. */
|
|
306
371
|
configPath?: string
|
|
@@ -312,6 +377,8 @@ export interface NormalizedVoltraConfig {
|
|
|
312
377
|
android?: NormalizedVoltraAndroidConfig
|
|
313
378
|
/** Normalized iOS-specific Voltra configuration. */
|
|
314
379
|
ios?: NormalizedVoltraIOSConfig
|
|
380
|
+
/** Non-fatal config problems, surfaced in the `voltra apply` summary. */
|
|
381
|
+
warnings?: string[]
|
|
315
382
|
}
|
|
316
383
|
|
|
317
384
|
export type CliDefaults = typeof CLI_DEFAULTS
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { iosWidgetKind, iosWidgetKindOverrides } from './widgetKind.ts'
|
|
5
|
+
|
|
6
|
+
describe('iosWidgetKind', () => {
|
|
7
|
+
test('defaults to the prefixed widget id', () => {
|
|
8
|
+
assert.equal(iosWidgetKind({ id: 'weather' }), 'Voltra_Widget_weather')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('uses the pinned kind when the widget has one', () => {
|
|
12
|
+
assert.equal(iosWidgetKind({ id: 'streak', kind: 'StreakWidget' }), 'StreakWidget')
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('iosWidgetKindOverrides', () => {
|
|
17
|
+
test('is undefined when no widget pins a kind, so the Info.plist key stays absent', () => {
|
|
18
|
+
assert.equal(iosWidgetKindOverrides([{ id: 'weather' }, { id: 'streak' }]), undefined)
|
|
19
|
+
assert.equal(iosWidgetKindOverrides([]), undefined)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('maps only the widgets that pin a kind', () => {
|
|
23
|
+
const overrides = iosWidgetKindOverrides([{ id: 'weather' }, { id: 'streak', kind: 'StreakWidget' }])
|
|
24
|
+
|
|
25
|
+
assert.deepEqual(overrides, { streak: 'StreakWidget' })
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IOSWidgetConfig } from './types'
|
|
2
|
+
|
|
3
|
+
/** Default prefix of a widget's WidgetKit `kind`: `Voltra_Widget_<id>`. Mirrors `VoltraStorageKeys.widgetKindPrefix`. */
|
|
4
|
+
export const IOS_WIDGET_KIND_PREFIX = 'Voltra_Widget_'
|
|
5
|
+
|
|
6
|
+
/** WidgetKit `kind` of a widget: the `kind` option when set, else `Voltra_Widget_<id>`. */
|
|
7
|
+
export function iosWidgetKind(widget: Pick<IOSWidgetConfig, 'id' | 'kind'>): string {
|
|
8
|
+
return widget.kind ?? `${IOS_WIDGET_KIND_PREFIX}${widget.id}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `{ id: kind }` for the widgets that pin a custom `kind`, or undefined when none does.
|
|
13
|
+
* Written to both Info.plists (`Voltra_WidgetKinds`) so native code maps ids to kinds and back.
|
|
14
|
+
*/
|
|
15
|
+
export function iosWidgetKindOverrides(
|
|
16
|
+
widgets: Pick<IOSWidgetConfig, 'id' | 'kind'>[]
|
|
17
|
+
): Record<string, string> | undefined {
|
|
18
|
+
const overrides: Record<string, string> = {}
|
|
19
|
+
|
|
20
|
+
for (const widget of widgets) {
|
|
21
|
+
if (widget.kind !== undefined) {
|
|
22
|
+
overrides[widget.id] = widget.kind
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return Object.keys(overrides).length > 0 ? overrides : undefined
|
|
27
|
+
}
|
|
@@ -36,6 +36,17 @@ export function requirePlatformPackage<TPackage>(projectRoot: string, platform:
|
|
|
36
36
|
return createProjectRequire(projectRoot)(getPlatformPackageName(platform)) as TPackage
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export function getPlatformClientPackageVersion(projectRoot: string, platform: VoltraPlatform): string {
|
|
40
|
+
const packageName = getPlatformClientPackageName(platform)
|
|
41
|
+
const manifest = createProjectRequire(projectRoot)(`${packageName}/package.json`) as { version?: unknown }
|
|
42
|
+
|
|
43
|
+
if (typeof manifest.version !== 'string' || manifest.version.length === 0) {
|
|
44
|
+
throw new Error(`Package ${packageName} does not declare a valid version.`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return manifest.version
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
export function getMissingPlatformPackageMessage(
|
|
40
51
|
platform: VoltraPlatform,
|
|
41
52
|
packageNames = getRequiredPlatformPackageNames(platform)
|
package/src/discovery/ios.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path'
|
|
|
4
4
|
import type { Stats } from 'node:fs'
|
|
5
5
|
|
|
6
6
|
import { VoltraCliError } from '../reporting/summary'
|
|
7
|
+
import { getValuesByBuildConfiguration, isPerConfigurationMap } from '../config/perConfiguration'
|
|
7
8
|
|
|
8
9
|
import type { NormalizedIOSProjectConfig } from '../config/types'
|
|
9
10
|
|
|
@@ -19,8 +20,18 @@ export interface IOSProjectDiscovery {
|
|
|
19
20
|
podfilePath: string
|
|
20
21
|
mainTargetName: string
|
|
21
22
|
mainTargetCandidates: string[]
|
|
23
|
+
/** Build configuration names of the app target, default build configuration first. */
|
|
24
|
+
buildConfigurationNames: string[]
|
|
25
|
+
/** Info.plist of the default build configuration. Always the first entry of `infoPlistPaths`. */
|
|
22
26
|
infoPlistPath: string
|
|
27
|
+
/** Every distinct Info.plist the app target references, default build configuration first. */
|
|
28
|
+
infoPlistPaths: string[]
|
|
29
|
+
/** Entitlements file of the default build configuration, when the project references one. */
|
|
23
30
|
entitlementsPath?: string
|
|
31
|
+
/** Every distinct entitlements file the app target references, default build configuration first. */
|
|
32
|
+
entitlementsPaths: string[]
|
|
33
|
+
/** Entitlements file per build configuration, when the config file names one per configuration. */
|
|
34
|
+
entitlementsPathByConfiguration?: Map<string, string>
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
interface ParsedPbxNativeTarget {
|
|
@@ -72,8 +83,13 @@ export async function discoverIOSProject(
|
|
|
72
83
|
const mainTargetCandidates = getMainTargetCandidates(parsedProject.targets)
|
|
73
84
|
const mainTarget = resolveMainTarget(mainTargetCandidates, config.mainTargetName, pbxprojPath)
|
|
74
85
|
const mainTargetBuildConfigurations = getTargetBuildConfigurations(parsedProject, mainTarget, pbxprojPath)
|
|
75
|
-
const
|
|
76
|
-
const
|
|
86
|
+
const infoPlistPaths = await resolveInfoPlistPaths(iosRoot, config, mainTarget, mainTargetBuildConfigurations)
|
|
87
|
+
const { entitlementsPaths, entitlementsPathByConfiguration } = await resolveEntitlements(
|
|
88
|
+
iosRoot,
|
|
89
|
+
config,
|
|
90
|
+
mainTarget,
|
|
91
|
+
mainTargetBuildConfigurations
|
|
92
|
+
)
|
|
77
93
|
|
|
78
94
|
return {
|
|
79
95
|
iosRoot,
|
|
@@ -82,8 +98,12 @@ export async function discoverIOSProject(
|
|
|
82
98
|
podfilePath,
|
|
83
99
|
mainTargetName: mainTarget.name,
|
|
84
100
|
mainTargetCandidates: mainTargetCandidates.map((target) => target.name).sort(),
|
|
85
|
-
|
|
86
|
-
|
|
101
|
+
buildConfigurationNames: mainTargetBuildConfigurations.map((configuration) => configuration.name),
|
|
102
|
+
infoPlistPath: infoPlistPaths[0],
|
|
103
|
+
infoPlistPaths,
|
|
104
|
+
entitlementsPath: entitlementsPaths[0],
|
|
105
|
+
entitlementsPaths,
|
|
106
|
+
entitlementsPathByConfiguration,
|
|
87
107
|
}
|
|
88
108
|
}
|
|
89
109
|
|
|
@@ -343,75 +363,102 @@ function getTargetBuildConfigurations(
|
|
|
343
363
|
: buildConfigurations
|
|
344
364
|
}
|
|
345
365
|
|
|
346
|
-
async function
|
|
366
|
+
async function resolveInfoPlistPaths(
|
|
347
367
|
iosRoot: string,
|
|
348
368
|
config: NormalizedIOSProjectConfig,
|
|
349
369
|
target: ParsedPbxNativeTarget,
|
|
350
370
|
buildConfigurations: ParsedXCBuildConfiguration[]
|
|
351
|
-
): Promise<string> {
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
371
|
+
): Promise<string[]> {
|
|
372
|
+
const discoveredPaths = config.infoPlistPath
|
|
373
|
+
? new Map<string, string[]>()
|
|
374
|
+
: resolveBuildSettingPaths(
|
|
375
|
+
iosRoot,
|
|
376
|
+
target,
|
|
377
|
+
buildConfigurations,
|
|
378
|
+
(configuration) => configuration.buildSettings.infoPlistFile
|
|
379
|
+
)
|
|
380
|
+
const infoPlistPaths = config.infoPlistPath ? [config.infoPlistPath] : [...discoveredPaths.keys()]
|
|
361
381
|
|
|
362
|
-
if (
|
|
382
|
+
if (infoPlistPaths.length === 0) {
|
|
363
383
|
throw new IOSProjectDiscoveryError(
|
|
364
384
|
`Could not determine Info.plist for target '${target.name}'. Set ios.project.infoPlistPath explicitly.`
|
|
365
385
|
)
|
|
366
386
|
}
|
|
367
387
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
388
|
+
for (const infoPlistPath of infoPlistPaths) {
|
|
389
|
+
await ensureFile(
|
|
390
|
+
infoPlistPath,
|
|
391
|
+
config.infoPlistPath
|
|
392
|
+
? `Configured Info.plist does not exist: ${infoPlistPath}`
|
|
393
|
+
: `Discovered Info.plist does not exist: ${infoPlistPath}${describeReferencingBuildConfigurations(
|
|
394
|
+
discoveredPaths,
|
|
395
|
+
infoPlistPath
|
|
396
|
+
)}. Set ios.project.infoPlistPath to override discovery.`
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return infoPlistPaths
|
|
401
|
+
}
|
|
374
402
|
|
|
375
|
-
|
|
403
|
+
interface ResolvedEntitlements {
|
|
404
|
+
entitlementsPaths: string[]
|
|
405
|
+
entitlementsPathByConfiguration?: Map<string, string>
|
|
376
406
|
}
|
|
377
407
|
|
|
378
|
-
async function
|
|
408
|
+
async function resolveEntitlements(
|
|
379
409
|
iosRoot: string,
|
|
380
410
|
config: NormalizedIOSProjectConfig,
|
|
381
411
|
target: ParsedPbxNativeTarget,
|
|
382
412
|
buildConfigurations: ParsedXCBuildConfiguration[]
|
|
383
|
-
): Promise<
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
413
|
+
): Promise<ResolvedEntitlements> {
|
|
414
|
+
const entitlementsPathByConfiguration = isPerConfigurationMap(config.entitlementsPath)
|
|
415
|
+
? getValuesByBuildConfiguration(
|
|
416
|
+
config.entitlementsPath,
|
|
417
|
+
buildConfigurations.map((configuration) => configuration.name),
|
|
418
|
+
'ios.project.entitlementsPath',
|
|
419
|
+
(message) => new IOSProjectDiscoveryError(message)
|
|
420
|
+
)
|
|
421
|
+
: undefined
|
|
422
|
+
const discoveredPaths = config.entitlementsPath
|
|
423
|
+
? new Map<string, string[]>()
|
|
424
|
+
: resolveBuildSettingPaths(
|
|
425
|
+
iosRoot,
|
|
426
|
+
target,
|
|
427
|
+
buildConfigurations,
|
|
428
|
+
(configuration) => configuration.buildSettings.codeSignEntitlements
|
|
429
|
+
)
|
|
430
|
+
const entitlementsPaths = entitlementsPathByConfiguration
|
|
431
|
+
? [...new Set(entitlementsPathByConfiguration.values())]
|
|
432
|
+
: typeof config.entitlementsPath === 'string'
|
|
433
|
+
? [config.entitlementsPath]
|
|
434
|
+
: [...discoveredPaths.keys()]
|
|
435
|
+
|
|
436
|
+
for (const entitlementsPath of entitlementsPaths) {
|
|
437
|
+
await ensureFile(
|
|
438
|
+
entitlementsPath,
|
|
439
|
+
config.entitlementsPath
|
|
440
|
+
? `Configured entitlements file does not exist: ${entitlementsPath}`
|
|
441
|
+
: `Discovered entitlements file does not exist: ${entitlementsPath}${describeReferencingBuildConfigurations(
|
|
442
|
+
discoveredPaths,
|
|
443
|
+
entitlementsPath
|
|
444
|
+
)}. Set ios.project.entitlementsPath to override discovery.`
|
|
392
445
|
)
|
|
393
|
-
|
|
394
|
-
if (!entitlementsPath) {
|
|
395
|
-
return undefined
|
|
396
446
|
}
|
|
397
447
|
|
|
398
|
-
|
|
399
|
-
entitlementsPath,
|
|
400
|
-
config.entitlementsPath
|
|
401
|
-
? `Configured entitlements file does not exist: ${entitlementsPath}`
|
|
402
|
-
: `Discovered entitlements file does not exist: ${entitlementsPath}`
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
return entitlementsPath
|
|
448
|
+
return { entitlementsPaths, entitlementsPathByConfiguration }
|
|
406
449
|
}
|
|
407
450
|
|
|
408
|
-
|
|
451
|
+
/**
|
|
452
|
+
* Collects the distinct paths a build setting resolves to across the target's build configurations.
|
|
453
|
+
* Apps that ship several environments point each configuration at its own file, so divergence is
|
|
454
|
+
* expected rather than an error.
|
|
455
|
+
*/
|
|
456
|
+
function resolveBuildSettingPaths(
|
|
409
457
|
iosRoot: string,
|
|
410
458
|
target: ParsedPbxNativeTarget,
|
|
411
459
|
buildConfigurations: ParsedXCBuildConfiguration[],
|
|
412
|
-
settingName: string,
|
|
413
460
|
getSettingValue: (configuration: ParsedXCBuildConfiguration) => string | undefined
|
|
414
|
-
): string
|
|
461
|
+
): Map<string, string[]> {
|
|
415
462
|
const resolvedPaths = new Map<string, string[]>()
|
|
416
463
|
|
|
417
464
|
for (const configuration of buildConfigurations) {
|
|
@@ -423,25 +470,22 @@ function resolveConsistentBuildSettingPath(
|
|
|
423
470
|
|
|
424
471
|
const resolvedPath = resolveXcodePathValue(settingValue, iosRoot, target, configuration)
|
|
425
472
|
const configurationNames = resolvedPaths.get(resolvedPath) ?? []
|
|
473
|
+
|
|
426
474
|
configurationNames.push(configuration.name)
|
|
427
475
|
resolvedPaths.set(resolvedPath, configurationNames)
|
|
428
476
|
}
|
|
429
477
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
478
|
+
return resolvedPaths
|
|
479
|
+
}
|
|
433
480
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
} explicitly.`
|
|
441
|
-
)
|
|
442
|
-
}
|
|
481
|
+
/** Names the build configurations that reference a path, so a missing file can be traced back. */
|
|
482
|
+
function describeReferencingBuildConfigurations(
|
|
483
|
+
pathsByBuildConfiguration: Map<string, string[]>,
|
|
484
|
+
filePath: string
|
|
485
|
+
): string {
|
|
486
|
+
const configurationNames = pathsByBuildConfiguration.get(filePath)
|
|
443
487
|
|
|
444
|
-
return
|
|
488
|
+
return configurationNames?.length ? ` (referenced by ${configurationNames.join(', ')})` : ''
|
|
445
489
|
}
|
|
446
490
|
|
|
447
491
|
function resolveXcodePathValue(
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { describe, test } from 'node:test'
|
|
6
|
+
|
|
7
|
+
import { findMissingMetroPackageWarning } from './metro.ts'
|
|
8
|
+
|
|
9
|
+
import type { NormalizedVoltraConfig } from '../config/types.ts'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A project root with its own `package.json`, so `createRequire` has an anchor, and optionally a
|
|
13
|
+
* `node_modules/@use-voltra/metro` that resolves the `bundle-widgets` subpath the same way the real
|
|
14
|
+
* package does.
|
|
15
|
+
*/
|
|
16
|
+
async function createProject({ withMetroInstalled }: { withMetroInstalled: boolean }): Promise<string> {
|
|
17
|
+
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'voltra-metro-'))
|
|
18
|
+
await fs.writeFile(path.join(projectRoot, 'package.json'), JSON.stringify({ name: 'app', version: '1.0.0' }))
|
|
19
|
+
|
|
20
|
+
if (withMetroInstalled) {
|
|
21
|
+
const packageRoot = path.join(projectRoot, 'node_modules', '@use-voltra', 'metro')
|
|
22
|
+
await fs.mkdir(path.join(packageRoot, 'build', 'cjs', 'bin'), { recursive: true })
|
|
23
|
+
await fs.writeFile(path.join(packageRoot, 'build', 'cjs', 'bin', 'bundle-widgets.js'), 'module.exports = {}\n')
|
|
24
|
+
await fs.writeFile(
|
|
25
|
+
path.join(packageRoot, 'package.json'),
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
name: '@use-voltra/metro',
|
|
28
|
+
version: '2.3.0',
|
|
29
|
+
exports: { './bundle-widgets': './build/cjs/bin/bundle-widgets.js' },
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return projectRoot
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function configWith(projectRoot: string, entry: string | undefined): NormalizedVoltraConfig {
|
|
38
|
+
return {
|
|
39
|
+
projectRoot,
|
|
40
|
+
android: {
|
|
41
|
+
project: {},
|
|
42
|
+
widgets: [
|
|
43
|
+
{
|
|
44
|
+
id: 'portfolio',
|
|
45
|
+
displayName: 'Portfolio',
|
|
46
|
+
description: 'Your holdings',
|
|
47
|
+
targetCellWidth: 2,
|
|
48
|
+
targetCellHeight: 2,
|
|
49
|
+
entry,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
fonts: [],
|
|
53
|
+
},
|
|
54
|
+
warnings: [],
|
|
55
|
+
} as unknown as NormalizedVoltraConfig
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('metro package detection', () => {
|
|
59
|
+
test('warns when a Dynamic Widget is declared and the package is not installed', async () => {
|
|
60
|
+
const projectRoot = await createProject({ withMetroInstalled: false })
|
|
61
|
+
|
|
62
|
+
const warning = findMissingMetroPackageWarning(configWith(projectRoot, './widgets/portfolio.tsx'))
|
|
63
|
+
|
|
64
|
+
assert.match(warning ?? '', /@use-voltra\/metro/)
|
|
65
|
+
assert.match(warning ?? '', /prerendered initial state/)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('stays quiet when the package resolves', async () => {
|
|
69
|
+
const projectRoot = await createProject({ withMetroInstalled: true })
|
|
70
|
+
|
|
71
|
+
assert.equal(findMissingMetroPackageWarning(configWith(projectRoot, './widgets/portfolio.tsx')), undefined)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('stays quiet when no widget has an entry, however Metro is set up', async () => {
|
|
75
|
+
const projectRoot = await createProject({ withMetroInstalled: false })
|
|
76
|
+
|
|
77
|
+
assert.equal(findMissingMetroPackageWarning(configWith(projectRoot, undefined)), undefined)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('does not warn at a project whose Metro config never names withVoltra', async () => {
|
|
81
|
+
// A config delegating to a shared preset, or hand-composed from createVoltraMiddleware, is
|
|
82
|
+
// correctly wired and must stay quiet — the package resolving is the whole signal.
|
|
83
|
+
const projectRoot = await createProject({ withMetroInstalled: true })
|
|
84
|
+
await fs.writeFile(
|
|
85
|
+
path.join(projectRoot, 'metro.config.js'),
|
|
86
|
+
"module.exports = require('@myorg/metro-config')(__dirname)\n"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
assert.equal(findMissingMetroPackageWarning(configWith(projectRoot, './widgets/portfolio.tsx')), undefined)
|
|
90
|
+
})
|
|
91
|
+
})
|