voltra 2.2.0 → 2.3.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/apply/index.js +4 -1
- package/build/cjs/apply/index.js.map +1 -1
- package/build/cjs/config/normalize.js +131 -29
- 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/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 +4 -1
- package/build/esm/apply/index.js.map +1 -1
- package/build/esm/config/normalize.js +131 -29
- 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/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/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 +4 -1
- package/src/config/normalize.node.test.ts +61 -0
- package/src/config/normalize.ts +188 -44
- 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/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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Android widget sizing.
|
|
3
|
+
*
|
|
4
|
+
* Android 12+ (API 31+) places widgets by `targetCellWidth`/`targetCellHeight`, expressed in
|
|
5
|
+
* launcher grid cells. Android 11 and older ignore those attributes entirely and place widgets by
|
|
6
|
+
* `minWidth`/`minHeight`, expressed in dp, so a widget has to declare both sets to be placed
|
|
7
|
+
* correctly everywhere. Voltra widgets build against the host app's `minSdkVersion` — React
|
|
8
|
+
* Native's template sets 24 — so the pre-Android-12 path is not a legacy edge case.
|
|
9
|
+
* https://developer.android.com/develop/ui/compose/glance/create-app-widget
|
|
10
|
+
*
|
|
11
|
+
* Cell size varies by device, launcher and orientation, so converting cells to dp can only ever be
|
|
12
|
+
* an approximation. These are the figures Google publishes for a 5x4 handset grid, following the
|
|
13
|
+
* accompanying guidance to size widths from the portrait table and heights from the landscape
|
|
14
|
+
* table, because those are the smaller cells on each axis.
|
|
15
|
+
* https://developer.android.com/develop/ui/views/appwidgets/layouts
|
|
16
|
+
*
|
|
17
|
+
* The `voltra` CLI and the `@use-voltra/android-client` Expo plugin generate the same
|
|
18
|
+
* `appwidget-provider` XML from their own copy of this module; keep the two in sync.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Widget fields that determine the generated `appwidget-provider` sizing attributes. */
|
|
22
|
+
export interface AndroidWidgetSizingInput {
|
|
23
|
+
targetCellWidth: number
|
|
24
|
+
targetCellHeight: number
|
|
25
|
+
minWidth?: number
|
|
26
|
+
minHeight?: number
|
|
27
|
+
minCellWidth?: number
|
|
28
|
+
minCellHeight?: number
|
|
29
|
+
minResizeWidth?: number
|
|
30
|
+
minResizeHeight?: number
|
|
31
|
+
maxResizeWidth?: number
|
|
32
|
+
maxResizeHeight?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resize bounds, in dp, emitted only when set. `maxResize*` is honoured from Android 12 on. */
|
|
36
|
+
const RESIZE_BOUNDS = ['minResizeWidth', 'minResizeHeight', 'maxResizeWidth', 'maxResizeHeight'] as const
|
|
37
|
+
|
|
38
|
+
function cellsToWidthDp(cells: number): number {
|
|
39
|
+
return cells * 73 - 16
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function cellsToHeightDp(cells: number): number {
|
|
43
|
+
return cells * 66 - 15
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveMinimumSize(widget: AndroidWidgetSizingInput): { minWidth: number; minHeight: number } {
|
|
47
|
+
return {
|
|
48
|
+
minWidth: widget.minWidth ?? cellsToWidthDp(widget.minCellWidth ?? widget.targetCellWidth),
|
|
49
|
+
minHeight: widget.minHeight ?? cellsToHeightDp(widget.minCellHeight ?? widget.targetCellHeight),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Sizing attributes for a single widget, ordered as in Android's own reference example.
|
|
55
|
+
*
|
|
56
|
+
* `minWidth` and `minHeight` are always emitted, so a widget is placed at its intended size on
|
|
57
|
+
* Android 11 and older: an explicit dp value wins, then the deprecated cell count, then the
|
|
58
|
+
* target cell count.
|
|
59
|
+
*/
|
|
60
|
+
export function androidWidgetSizingAttributes(widget: AndroidWidgetSizingInput): string[] {
|
|
61
|
+
const { minWidth, minHeight } = resolveMinimumSize(widget)
|
|
62
|
+
|
|
63
|
+
const attributes = [
|
|
64
|
+
`android:minWidth="${minWidth}dp"`,
|
|
65
|
+
`android:minHeight="${minHeight}dp"`,
|
|
66
|
+
`android:targetCellWidth="${widget.targetCellWidth}"`,
|
|
67
|
+
`android:targetCellHeight="${widget.targetCellHeight}"`,
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
for (const bound of RESIZE_BOUNDS) {
|
|
71
|
+
const value = widget[bound]
|
|
72
|
+
if (value !== undefined) {
|
|
73
|
+
attributes.push(`android:${bound}="${value}dp"`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return attributes
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resize bounds Android will silently ignore, described for the developer who set them.
|
|
82
|
+
*
|
|
83
|
+
* `minResizeWidth` has no effect when it is greater than `minWidth`, and `maxResizeWidth` has no
|
|
84
|
+
* effect when it is smaller; likewise for the height pair. This reports rather than throws,
|
|
85
|
+
* because `minWidth` is usually the approximation derived from the widget's cell count.
|
|
86
|
+
* https://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo
|
|
87
|
+
*/
|
|
88
|
+
export function androidWidgetSizingWarnings(widget: AndroidWidgetSizingInput, widgetId: string): string[] {
|
|
89
|
+
const { minWidth, minHeight } = resolveMinimumSize(widget)
|
|
90
|
+
const warnings: string[] = []
|
|
91
|
+
|
|
92
|
+
const report = (bound: string, value: number, relation: string, minimum: string, minimumValue: number): void => {
|
|
93
|
+
warnings.push(
|
|
94
|
+
`Widget '${widgetId}': ${bound} (${value}dp) is ${relation} ${minimum} (${minimumValue}dp), so Android ignores it.`
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (widget.minResizeWidth !== undefined && widget.minResizeWidth > minWidth) {
|
|
99
|
+
report('minResizeWidth', widget.minResizeWidth, 'greater than', 'minWidth', minWidth)
|
|
100
|
+
}
|
|
101
|
+
if (widget.minResizeHeight !== undefined && widget.minResizeHeight > minHeight) {
|
|
102
|
+
report('minResizeHeight', widget.minResizeHeight, 'greater than', 'minHeight', minHeight)
|
|
103
|
+
}
|
|
104
|
+
if (widget.maxResizeWidth !== undefined && widget.maxResizeWidth < minWidth) {
|
|
105
|
+
report('maxResizeWidth', widget.maxResizeWidth, 'smaller than', 'minWidth', minWidth)
|
|
106
|
+
}
|
|
107
|
+
if (widget.maxResizeHeight !== undefined && widget.maxResizeHeight < minHeight) {
|
|
108
|
+
report('maxResizeHeight', widget.maxResizeHeight, 'smaller than', 'minHeight', minHeight)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return warnings
|
|
112
|
+
}
|
|
@@ -2,6 +2,7 @@ import { discoverIOSProject } from '../../discovery/ios'
|
|
|
2
2
|
import { getMissingPlatformPackageMessage, getMissingPlatformPackages } from '../../dependencies/platformPackages'
|
|
3
3
|
import { VoltraCliError } from '../../reporting/summary'
|
|
4
4
|
|
|
5
|
+
import { resolveIOSBuildConfigurationValues } from './buildConfigurationValues'
|
|
5
6
|
import { ensureEntitlements } from './entitlements'
|
|
6
7
|
import { generateIOSFiles } from './generated'
|
|
7
8
|
import { ensureInfoPlist } from './plist'
|
|
@@ -52,32 +53,36 @@ export async function applyIOSPlatform(context: PlatformApplyContext): Promise<P
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const discovery = getIOSDiscovery(context.preflight)
|
|
56
|
+
// Values given per build configuration are collapsed once, so every mutator below sees a plain
|
|
57
|
+
// string and the project carries the per-configuration values.
|
|
58
|
+
const { ios, buildSettings } = resolveIOSBuildConfigurationValues(iosConfig, discovery)
|
|
55
59
|
const generatedResult = await generateIOSFiles({
|
|
56
60
|
projectRoot: context.config.projectRoot,
|
|
57
|
-
ios
|
|
61
|
+
ios,
|
|
58
62
|
discovery,
|
|
59
63
|
})
|
|
60
64
|
const entitlementsResult = await ensureEntitlements({
|
|
61
65
|
projectRoot: context.config.projectRoot,
|
|
62
|
-
ios
|
|
66
|
+
ios,
|
|
63
67
|
discovery,
|
|
64
68
|
})
|
|
65
69
|
const infoPlistResult = await ensureInfoPlist({
|
|
66
70
|
projectRoot: context.config.projectRoot,
|
|
67
|
-
ios
|
|
71
|
+
ios,
|
|
68
72
|
discovery,
|
|
69
73
|
})
|
|
70
74
|
const podfileResult = await ensurePodfileBlock({
|
|
71
75
|
projectRoot: context.config.projectRoot,
|
|
72
|
-
ios
|
|
76
|
+
ios,
|
|
73
77
|
discovery,
|
|
74
78
|
})
|
|
75
79
|
const xcodeTargetResult = await ensureIOSWidgetTarget({
|
|
76
80
|
projectRoot: context.config.projectRoot,
|
|
77
|
-
ios
|
|
81
|
+
ios,
|
|
78
82
|
discovery,
|
|
79
83
|
generatedFiles: generatedResult.files,
|
|
80
84
|
previousGeneratedFiles: context.previousState?.files,
|
|
85
|
+
buildSettings,
|
|
81
86
|
})
|
|
82
87
|
|
|
83
88
|
if (generatedResult.targetName !== xcodeTargetResult.targetName) {
|
|
@@ -93,11 +98,10 @@ export async function applyIOSPlatform(context: PlatformApplyContext): Promise<P
|
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
const changes = [
|
|
96
|
-
infoPlistResult.
|
|
97
|
-
entitlementsResult.
|
|
98
|
-
podfileResult.change,
|
|
99
|
-
|
|
100
|
-
].filter(isDefined)
|
|
101
|
+
...infoPlistResult.changes,
|
|
102
|
+
...entitlementsResult.changes,
|
|
103
|
+
...[podfileResult.change, xcodeTargetResult.change].filter(isDefined),
|
|
104
|
+
]
|
|
101
105
|
|
|
102
106
|
return {
|
|
103
107
|
platform: 'ios',
|
|
@@ -130,7 +134,13 @@ function isIOSProjectDiscovery(value: unknown): value is IOSProjectDiscovery {
|
|
|
130
134
|
candidate.podfilePath,
|
|
131
135
|
candidate.mainTargetName,
|
|
132
136
|
candidate.infoPlistPath,
|
|
133
|
-
].every((entry) => typeof entry === 'string' && entry.length > 0) &&
|
|
137
|
+
].every((entry) => typeof entry === 'string' && entry.length > 0) &&
|
|
138
|
+
[
|
|
139
|
+
candidate.mainTargetCandidates,
|
|
140
|
+
candidate.buildConfigurationNames,
|
|
141
|
+
candidate.infoPlistPaths,
|
|
142
|
+
candidate.entitlementsPaths,
|
|
143
|
+
].every((entry) => Array.isArray(entry))
|
|
134
144
|
)
|
|
135
145
|
}
|
|
136
146
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { getValuesByBuildConfiguration, isPerConfigurationMap } from '../../config/perConfiguration'
|
|
2
|
+
import { VoltraCliError } from '../../reporting/summary'
|
|
3
|
+
|
|
4
|
+
import type { PerConfiguration } from '../../config/perConfiguration'
|
|
5
|
+
import type { NormalizedVoltraIOSConfig, ResolvedVoltraIOSConfig } from '../../config/types'
|
|
6
|
+
import type { IOSProjectDiscovery } from '../../discovery/ios'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prefix of the user-defined build settings Voltra writes. Settings under it are Voltra's to add
|
|
10
|
+
* and remove, so a value that stops being per build configuration leaves nothing behind.
|
|
11
|
+
*/
|
|
12
|
+
export const VOLTRA_BUILD_SETTING_PREFIX = 'VOLTRA_'
|
|
13
|
+
|
|
14
|
+
const APP_GROUP_IDENTIFIER_SETTING = `${VOLTRA_BUILD_SETTING_PREFIX}APP_GROUP_IDENTIFIER`
|
|
15
|
+
const KEYCHAIN_GROUP_SETTING = `${VOLTRA_BUILD_SETTING_PREFIX}KEYCHAIN_GROUP`
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The settings Voltra writes and is therefore allowed to remove. Anything else a project happens to
|
|
19
|
+
* name `VOLTRA_*` belongs to the user and is left alone.
|
|
20
|
+
*/
|
|
21
|
+
export const VOLTRA_OWNED_BUILD_SETTINGS: readonly string[] = [APP_GROUP_IDENTIFIER_SETTING, KEYCHAIN_GROUP_SETTING]
|
|
22
|
+
|
|
23
|
+
export interface IOSBuildConfigurationValues {
|
|
24
|
+
/** iOS config with every per-build-configuration value collapsed to a single string. */
|
|
25
|
+
ios: ResolvedVoltraIOSConfig
|
|
26
|
+
/** User-defined build settings to write, keyed by build configuration name. */
|
|
27
|
+
buildSettings: Map<string, Record<string, string>>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class IOSBuildConfigurationValueError extends VoltraCliError {
|
|
31
|
+
constructor(message: string) {
|
|
32
|
+
super(message, 'VOLTRA_IOS_BUILD_CONFIGURATION_VALUE_FAILED')
|
|
33
|
+
this.name = 'IOSBuildConfigurationValueError'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Collapses per-build-configuration values into the single strings the platform mutators consume.
|
|
39
|
+
*
|
|
40
|
+
* A plain string resolves to itself and writes no build settings. A value given per build
|
|
41
|
+
* configuration becomes a user-defined build setting holding one value per configuration, and
|
|
42
|
+
* resolves to a reference to that setting, which Xcode expands per build into the entitlements and
|
|
43
|
+
* Info.plist files. The app and the generated widget extension both read the value that way, so
|
|
44
|
+
* nothing downstream has to know which environment is being built.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveIOSBuildConfigurationValues(
|
|
47
|
+
ios: NormalizedVoltraIOSConfig,
|
|
48
|
+
discovery: IOSProjectDiscovery
|
|
49
|
+
): IOSBuildConfigurationValues {
|
|
50
|
+
const buildSettings = new Map<string, Record<string, string>>()
|
|
51
|
+
|
|
52
|
+
const resolve = (
|
|
53
|
+
settingName: string,
|
|
54
|
+
value: PerConfiguration<string> | undefined,
|
|
55
|
+
context: string
|
|
56
|
+
): string | undefined => {
|
|
57
|
+
if (!isPerConfigurationMap(value)) {
|
|
58
|
+
return value
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const valuesByBuildConfiguration = getValuesByBuildConfiguration(
|
|
62
|
+
value,
|
|
63
|
+
discovery.buildConfigurationNames,
|
|
64
|
+
context,
|
|
65
|
+
(message) => new IOSBuildConfigurationValueError(message)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
for (const [buildConfigurationName, configurationValue] of valuesByBuildConfiguration) {
|
|
69
|
+
const configurationSettings = buildSettings.get(buildConfigurationName) ?? {}
|
|
70
|
+
configurationSettings[settingName] = configurationValue
|
|
71
|
+
buildSettings.set(buildConfigurationName, configurationSettings)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return `$(${settingName})`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
ios: {
|
|
79
|
+
...ios,
|
|
80
|
+
groupIdentifier: resolve(APP_GROUP_IDENTIFIER_SETTING, ios.groupIdentifier, 'ios.groupIdentifier'),
|
|
81
|
+
keychainGroup: resolve(KEYCHAIN_GROUP_SETTING, ios.keychainGroup, 'ios.keychainGroup'),
|
|
82
|
+
project: {
|
|
83
|
+
...ios.project,
|
|
84
|
+
entitlementsPath: resolveEntitlementsPath(ios.project.entitlementsPath, discovery),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
buildSettings,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The entitlements file of the default build configuration. Unlike a value written into a file, a
|
|
93
|
+
* per-configuration entitlements path is a build setting of its own, so it is applied to the Xcode
|
|
94
|
+
* project directly rather than through a reference.
|
|
95
|
+
*/
|
|
96
|
+
function resolveEntitlementsPath(
|
|
97
|
+
entitlementsPath: PerConfiguration<string> | undefined,
|
|
98
|
+
discovery: IOSProjectDiscovery
|
|
99
|
+
): string | undefined {
|
|
100
|
+
if (!isPerConfigurationMap(entitlementsPath)) {
|
|
101
|
+
return entitlementsPath
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return discovery.entitlementsPathByConfiguration?.get(discovery.buildConfigurationNames[0])
|
|
105
|
+
}
|
|
@@ -6,7 +6,7 @@ import { buildPlistXml, parsePlistFile } from './plist'
|
|
|
6
6
|
import { resolveMainAppEntitlementsPath } from './mainAppEntitlements'
|
|
7
7
|
|
|
8
8
|
import type { IOSProjectDiscovery } from '../../discovery/ios'
|
|
9
|
-
import type {
|
|
9
|
+
import type { ResolvedVoltraIOSConfig } from '../../config/types'
|
|
10
10
|
import type { ReportedChange } from '../../reporting/summary'
|
|
11
11
|
|
|
12
12
|
interface PreviousVoltraEntitlementValues {
|
|
@@ -17,12 +17,12 @@ interface PreviousVoltraEntitlementValues {
|
|
|
17
17
|
|
|
18
18
|
export interface EnsureEntitlementsOptions {
|
|
19
19
|
projectRoot: string
|
|
20
|
-
ios:
|
|
20
|
+
ios: ResolvedVoltraIOSConfig
|
|
21
21
|
discovery: IOSProjectDiscovery
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export interface EnsureEntitlementsResult {
|
|
25
|
-
|
|
25
|
+
changes: ReportedChange[]
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export class IOSEntitlementsMutationError extends VoltraCliError {
|
|
@@ -34,44 +34,68 @@ export class IOSEntitlementsMutationError extends VoltraCliError {
|
|
|
34
34
|
|
|
35
35
|
export async function ensureEntitlements(options: EnsureEntitlementsOptions): Promise<EnsureEntitlementsResult> {
|
|
36
36
|
const { projectRoot, ios, discovery } = options
|
|
37
|
-
const
|
|
37
|
+
const entitlementsPaths = resolveEntitlementsPathsToMutate(ios, discovery)
|
|
38
38
|
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
return {}
|
|
42
|
-
}
|
|
39
|
+
if (entitlementsPaths.length === 0) {
|
|
40
|
+
return { changes: [] }
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
const entitlements: Record<string, unknown> = (await pathExists(entitlementsPath))
|
|
46
|
-
? await parsePlistFile(entitlementsPath, 'main app entitlements', createEntitlementsError)
|
|
47
|
-
: {}
|
|
48
43
|
const previousVoltraValues = await readPreviousVoltraEntitlementValues(discovery.infoPlistPath)
|
|
44
|
+
const changes: ReportedChange[] = []
|
|
45
|
+
|
|
46
|
+
for (const entitlementsPath of entitlementsPaths) {
|
|
47
|
+
const entitlements: Record<string, unknown> = (await pathExists(entitlementsPath))
|
|
48
|
+
? await parsePlistFile(entitlementsPath, 'main app entitlements', createEntitlementsError)
|
|
49
|
+
: {}
|
|
50
|
+
|
|
51
|
+
ensureStringArrayValue(
|
|
52
|
+
entitlements,
|
|
53
|
+
'com.apple.security.application-groups',
|
|
54
|
+
ios.groupIdentifier,
|
|
55
|
+
previousVoltraValues.appGroupIdentifier
|
|
56
|
+
)
|
|
57
|
+
ensureStringArrayValue(
|
|
58
|
+
entitlements,
|
|
59
|
+
'keychain-access-groups',
|
|
60
|
+
ios.keychainGroup,
|
|
61
|
+
previousVoltraValues.keychainGroup
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if (ios.enablePushNotifications && entitlements['aps-environment'] === undefined) {
|
|
65
|
+
entitlements['aps-environment'] = 'development'
|
|
66
|
+
} else if (
|
|
67
|
+
!ios.enablePushNotifications &&
|
|
68
|
+
previousVoltraValues.pushNotificationsEnabled &&
|
|
69
|
+
entitlements['aps-environment'] === 'development'
|
|
70
|
+
) {
|
|
71
|
+
delete entitlements['aps-environment']
|
|
72
|
+
}
|
|
49
73
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
ensureStringArrayValue(entitlements, 'keychain-access-groups', ios.keychainGroup, previousVoltraValues.keychainGroup)
|
|
57
|
-
|
|
58
|
-
if (ios.enablePushNotifications && entitlements['aps-environment'] === undefined) {
|
|
59
|
-
entitlements['aps-environment'] = 'development'
|
|
60
|
-
} else if (
|
|
61
|
-
!ios.enablePushNotifications &&
|
|
62
|
-
previousVoltraValues.pushNotificationsEnabled &&
|
|
63
|
-
entitlements['aps-environment'] === 'development'
|
|
64
|
-
) {
|
|
65
|
-
delete entitlements['aps-environment']
|
|
74
|
+
const nextContent = buildPlistXml(entitlements, createEntitlementsError)
|
|
75
|
+
const change = await writeEntitlementsIfChanged(projectRoot, entitlementsPath, nextContent)
|
|
76
|
+
|
|
77
|
+
if (change) {
|
|
78
|
+
changes.push(change)
|
|
79
|
+
}
|
|
66
80
|
}
|
|
67
81
|
|
|
68
|
-
|
|
69
|
-
|
|
82
|
+
return { changes }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Every entitlements file the app target references, so each build configuration of a
|
|
87
|
+
* multi-environment app keeps its own file. Falls back to the standard path when the project
|
|
88
|
+
* references none and Voltra has values to write.
|
|
89
|
+
*/
|
|
90
|
+
function resolveEntitlementsPathsToMutate(ios: ResolvedVoltraIOSConfig, discovery: IOSProjectDiscovery): string[] {
|
|
91
|
+
if (discovery.entitlementsPaths.length > 0) {
|
|
92
|
+
return discovery.entitlementsPaths
|
|
93
|
+
}
|
|
70
94
|
|
|
71
|
-
return
|
|
95
|
+
return needsEntitlementsMutation(ios) ? [resolveMainAppEntitlementsPath(discovery)] : []
|
|
72
96
|
}
|
|
73
97
|
|
|
74
|
-
export function needsEntitlementsMutation(ios:
|
|
98
|
+
export function needsEntitlementsMutation(ios: ResolvedVoltraIOSConfig): boolean {
|
|
75
99
|
return ios.enablePushNotifications || ios.groupIdentifier !== undefined || ios.keychainGroup !== undefined
|
|
76
100
|
}
|
|
77
101
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { __test__ } from './generated.ts'
|
|
5
|
+
|
|
6
|
+
import type { DetectedIOSWidget } from './generated.ts'
|
|
7
|
+
|
|
8
|
+
describe('generateWidgetBundleSwift', () => {
|
|
9
|
+
test('imports the compiled VoltraRuntime module, not the VoltraWidget pod name', () => {
|
|
10
|
+
const swift = __test__.generateWidgetBundleSwift([])
|
|
11
|
+
|
|
12
|
+
assert.ok(swift.includes('import VoltraRuntime'))
|
|
13
|
+
assert.ok(!swift.includes('import VoltraWidget'))
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('imports VoltraRuntime when widgets are configured too', () => {
|
|
17
|
+
const widget: DetectedIOSWidget = {
|
|
18
|
+
id: 'weather',
|
|
19
|
+
displayName: 'Weather',
|
|
20
|
+
description: 'Shows weather',
|
|
21
|
+
supportedFamilies: ['systemSmall'],
|
|
22
|
+
clientRendered: false,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const swift = __test__.generateWidgetBundleSwift([widget])
|
|
26
|
+
|
|
27
|
+
assert.ok(swift.includes('import VoltraRuntime'))
|
|
28
|
+
assert.ok(!swift.includes('import VoltraWidget'))
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('defaults the WidgetKit kind to the prefixed widget id', () => {
|
|
32
|
+
const widget: DetectedIOSWidget = {
|
|
33
|
+
id: 'weather',
|
|
34
|
+
displayName: 'Weather',
|
|
35
|
+
description: 'Shows weather',
|
|
36
|
+
supportedFamilies: ['systemSmall'],
|
|
37
|
+
clientRendered: false,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const swift = __test__.generateWidgetBundleSwift([widget])
|
|
41
|
+
|
|
42
|
+
assert.ok(swift.includes('kind: "Voltra_Widget_weather"'))
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('uses the `kind` option as the WidgetKit kind when set', () => {
|
|
46
|
+
// A widget migrated from a hand-written extension keeps the kind its placed instances use.
|
|
47
|
+
const widget: DetectedIOSWidget = {
|
|
48
|
+
id: 'streak',
|
|
49
|
+
kind: 'StreakWidget',
|
|
50
|
+
displayName: 'Streak',
|
|
51
|
+
description: 'Your daily streak',
|
|
52
|
+
supportedFamilies: ['systemSmall'],
|
|
53
|
+
clientRendered: false,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const swift = __test__.generateWidgetBundleSwift([widget])
|
|
57
|
+
|
|
58
|
+
assert.ok(swift.includes('kind: "StreakWidget"'))
|
|
59
|
+
assert.ok(!swift.includes('Voltra_Widget_streak'))
|
|
60
|
+
})
|
|
61
|
+
})
|