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.
Files changed (130) hide show
  1. package/build/cjs/apply/index.js +4 -1
  2. package/build/cjs/apply/index.js.map +1 -1
  3. package/build/cjs/config/normalize.js +131 -29
  4. package/build/cjs/config/normalize.js.map +1 -1
  5. package/build/cjs/config/perConfiguration.js +29 -0
  6. package/build/cjs/config/perConfiguration.js.map +1 -0
  7. package/build/cjs/config/serverUpdate.js +119 -0
  8. package/build/cjs/config/serverUpdate.js.map +1 -0
  9. package/build/cjs/config/widgetKind.js +25 -0
  10. package/build/cjs/config/widgetKind.js.map +1 -0
  11. package/build/cjs/dependencies/platformPackages.js +9 -0
  12. package/build/cjs/dependencies/platformPackages.js.map +1 -1
  13. package/build/cjs/discovery/ios.js +50 -31
  14. package/build/cjs/discovery/ios.js.map +1 -1
  15. package/build/cjs/index.js +6 -2
  16. package/build/cjs/index.js.map +1 -1
  17. package/build/cjs/platforms/android/generated.js +98 -40
  18. package/build/cjs/platforms/android/generated.js.map +1 -1
  19. package/build/cjs/platforms/android/widgetSizing.js +89 -0
  20. package/build/cjs/platforms/android/widgetSizing.js.map +1 -0
  21. package/build/cjs/platforms/ios/apply.js +21 -11
  22. package/build/cjs/platforms/ios/apply.js.map +1 -1
  23. package/build/cjs/platforms/ios/buildConfigurationValues.js +73 -0
  24. package/build/cjs/platforms/ios/buildConfigurationValues.js.map +1 -0
  25. package/build/cjs/platforms/ios/entitlements.js +34 -19
  26. package/build/cjs/platforms/ios/entitlements.js.map +1 -1
  27. package/build/cjs/platforms/ios/generated.js +79 -32
  28. package/build/cjs/platforms/ios/generated.js.map +1 -1
  29. package/build/cjs/platforms/ios/plist.js +22 -4
  30. package/build/cjs/platforms/ios/plist.js.map +1 -1
  31. package/build/cjs/platforms/ios/targetName.js.map +1 -1
  32. package/build/cjs/platforms/ios/xcodeTarget.js +166 -23
  33. package/build/cjs/platforms/ios/xcodeTarget.js.map +1 -1
  34. package/build/cjs/platforms/shared/widgetModule.js +18 -145
  35. package/build/cjs/platforms/shared/widgetModule.js.map +1 -1
  36. package/build/esm/apply/index.js +4 -1
  37. package/build/esm/apply/index.js.map +1 -1
  38. package/build/esm/config/normalize.js +131 -29
  39. package/build/esm/config/normalize.js.map +1 -1
  40. package/build/esm/config/perConfiguration.js +25 -0
  41. package/build/esm/config/perConfiguration.js.map +1 -0
  42. package/build/esm/config/serverUpdate.js +112 -0
  43. package/build/esm/config/serverUpdate.js.map +1 -0
  44. package/build/esm/config/widgetKind.js +20 -0
  45. package/build/esm/config/widgetKind.js.map +1 -0
  46. package/build/esm/dependencies/platformPackages.js +8 -0
  47. package/build/esm/dependencies/platformPackages.js.map +1 -1
  48. package/build/esm/discovery/ios.js +50 -31
  49. package/build/esm/discovery/ios.js.map +1 -1
  50. package/build/esm/index.js +1 -0
  51. package/build/esm/index.js.map +1 -1
  52. package/build/esm/platforms/android/generated.js +100 -42
  53. package/build/esm/platforms/android/generated.js.map +1 -1
  54. package/build/esm/platforms/android/widgetSizing.js +85 -0
  55. package/build/esm/platforms/android/widgetSizing.js.map +1 -0
  56. package/build/esm/platforms/ios/apply.js +21 -11
  57. package/build/esm/platforms/ios/apply.js.map +1 -1
  58. package/build/esm/platforms/ios/buildConfigurationValues.js +68 -0
  59. package/build/esm/platforms/ios/buildConfigurationValues.js.map +1 -0
  60. package/build/esm/platforms/ios/entitlements.js +34 -19
  61. package/build/esm/platforms/ios/entitlements.js.map +1 -1
  62. package/build/esm/platforms/ios/generated.js +80 -33
  63. package/build/esm/platforms/ios/generated.js.map +1 -1
  64. package/build/esm/platforms/ios/plist.js +22 -4
  65. package/build/esm/platforms/ios/plist.js.map +1 -1
  66. package/build/esm/platforms/ios/targetName.js.map +1 -1
  67. package/build/esm/platforms/ios/xcodeTarget.js +166 -23
  68. package/build/esm/platforms/ios/xcodeTarget.js.map +1 -1
  69. package/build/esm/platforms/shared/widgetModule.js +16 -107
  70. package/build/esm/platforms/shared/widgetModule.js.map +1 -1
  71. package/build/types/apply/index.d.ts.map +1 -1
  72. package/build/types/config/normalize.d.ts.map +1 -1
  73. package/build/types/config/perConfiguration.d.ts +18 -0
  74. package/build/types/config/perConfiguration.d.ts.map +1 -0
  75. package/build/types/config/serverUpdate.d.ts +80 -0
  76. package/build/types/config/serverUpdate.d.ts.map +1 -0
  77. package/build/types/config/types.d.ts +91 -30
  78. package/build/types/config/types.d.ts.map +1 -1
  79. package/build/types/config/widgetKind.d.ts +11 -0
  80. package/build/types/config/widgetKind.d.ts.map +1 -0
  81. package/build/types/dependencies/platformPackages.d.ts +1 -0
  82. package/build/types/dependencies/platformPackages.d.ts.map +1 -1
  83. package/build/types/discovery/ios.d.ts +10 -0
  84. package/build/types/discovery/ios.d.ts.map +1 -1
  85. package/build/types/index.d.ts +3 -1
  86. package/build/types/index.d.ts.map +1 -1
  87. package/build/types/platforms/android/generated.d.ts.map +1 -1
  88. package/build/types/platforms/android/widgetSizing.d.ts +50 -0
  89. package/build/types/platforms/android/widgetSizing.d.ts.map +1 -0
  90. package/build/types/platforms/ios/apply.d.ts.map +1 -1
  91. package/build/types/platforms/ios/buildConfigurationValues.d.ts +33 -0
  92. package/build/types/platforms/ios/buildConfigurationValues.d.ts.map +1 -0
  93. package/build/types/platforms/ios/entitlements.d.ts +4 -4
  94. package/build/types/platforms/ios/entitlements.d.ts.map +1 -1
  95. package/build/types/platforms/ios/generated.d.ts +14 -2
  96. package/build/types/platforms/ios/generated.d.ts.map +1 -1
  97. package/build/types/platforms/ios/plist.d.ts +3 -3
  98. package/build/types/platforms/ios/plist.d.ts.map +1 -1
  99. package/build/types/platforms/ios/podfile.d.ts +2 -2
  100. package/build/types/platforms/ios/podfile.d.ts.map +1 -1
  101. package/build/types/platforms/ios/targetName.d.ts +2 -2
  102. package/build/types/platforms/ios/targetName.d.ts.map +1 -1
  103. package/build/types/platforms/ios/xcodeTarget.d.ts +4 -2
  104. package/build/types/platforms/ios/xcodeTarget.d.ts.map +1 -1
  105. package/build/types/platforms/shared/widgetModule.d.ts +12 -3
  106. package/build/types/platforms/shared/widgetModule.d.ts.map +1 -1
  107. package/package.json +5 -5
  108. package/src/apply/index.ts +4 -1
  109. package/src/config/normalize.node.test.ts +61 -0
  110. package/src/config/normalize.ts +188 -44
  111. package/src/config/perConfiguration.ts +49 -0
  112. package/src/config/serverUpdate.ts +160 -0
  113. package/src/config/types.ts +97 -30
  114. package/src/config/widgetKind.node.test.ts +27 -0
  115. package/src/config/widgetKind.ts +27 -0
  116. package/src/dependencies/platformPackages.ts +11 -0
  117. package/src/discovery/ios.ts +104 -60
  118. package/src/index.ts +8 -0
  119. package/src/platforms/android/generated.ts +144 -47
  120. package/src/platforms/android/widgetSizing.ts +112 -0
  121. package/src/platforms/ios/apply.ts +21 -11
  122. package/src/platforms/ios/buildConfigurationValues.ts +105 -0
  123. package/src/platforms/ios/entitlements.ts +55 -31
  124. package/src/platforms/ios/generated.node.test.ts +61 -0
  125. package/src/platforms/ios/generated.ts +137 -38
  126. package/src/platforms/ios/plist.ts +36 -7
  127. package/src/platforms/ios/podfile.ts +2 -2
  128. package/src/platforms/ios/targetName.ts +2 -2
  129. package/src/platforms/ios/xcodeTarget.ts +248 -43
  130. package/src/platforms/shared/widgetModule.ts +22 -140
@@ -1,15 +1,15 @@
1
1
  import { VoltraCliError } from '../../reporting/summary';
2
2
  import type { IOSProjectDiscovery } from '../../discovery/ios';
3
- import type { NormalizedVoltraIOSConfig } from '../../config/types';
3
+ import type { ResolvedVoltraIOSConfig } from '../../config/types';
4
4
  import type { ReportedChange } from '../../reporting/summary';
5
5
  type PlistErrorFactory = (message: string) => Error;
6
6
  export interface EnsureInfoPlistOptions {
7
7
  projectRoot: string;
8
- ios: NormalizedVoltraIOSConfig;
8
+ ios: ResolvedVoltraIOSConfig;
9
9
  discovery: IOSProjectDiscovery;
10
10
  }
11
11
  export interface EnsureInfoPlistResult {
12
- change?: ReportedChange;
12
+ changes: ReportedChange[];
13
13
  }
14
14
  export declare class IOSInfoPlistMutationError extends VoltraCliError {
15
15
  constructor(message: string);
@@ -1 +1 @@
1
- {"version":3,"file":"plist.d.ts","sourceRoot":"","sources":["../../../../src/platforms/ios/plist.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAExD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAA;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAa7D,KAAK,iBAAiB,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,KAAK,CAAA;AAEnD,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,yBAAyB,CAAA;IAC9B,SAAS,EAAE,mBAAmB,CAAA;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,cAAc,CAAA;CACxB;AAED,qBAAa,yBAA0B,SAAQ,cAAc;gBAC/C,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAmCrG;AAED,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,iBAAiB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAuBlC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,iBAAiB,GAAG,MAAM,CAiBpF"}
1
+ {"version":3,"file":"plist.d.ts","sourceRoot":"","sources":["../../../../src/platforms/ios/plist.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAExD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAa7D,KAAK,iBAAiB,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,KAAK,CAAA;AAEnD,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,uBAAuB,CAAA;IAC5B,SAAS,EAAE,mBAAmB,CAAA;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,cAAc,EAAE,CAAA;CAC1B;AAED,qBAAa,yBAA0B,SAAQ,cAAc;gBAC/C,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAcrG;AAmDD,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,iBAAiB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAuBlC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,iBAAiB,GAAG,MAAM,CAiBpF"}
@@ -1,10 +1,10 @@
1
1
  import { VoltraCliError } from '../../reporting/summary';
2
2
  import type { IOSProjectDiscovery } from '../../discovery/ios';
3
- import type { NormalizedVoltraIOSConfig } from '../../config/types';
3
+ import type { ResolvedVoltraIOSConfig } from '../../config/types';
4
4
  import type { ReportedChange } from '../../reporting/summary';
5
5
  export interface EnsurePodfileBlockOptions {
6
6
  projectRoot: string;
7
- ios: NormalizedVoltraIOSConfig;
7
+ ios: ResolvedVoltraIOSConfig;
8
8
  discovery: IOSProjectDiscovery;
9
9
  }
10
10
  export interface EnsurePodfileBlockResult {
@@ -1 +1 @@
1
- {"version":3,"file":"podfile.d.ts","sourceRoot":"","sources":["../../../../src/platforms/ios/podfile.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAIxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAA;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAO7D,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,yBAAyB,CAAA;IAC9B,SAAS,EAAE,mBAAmB,CAAA;CAC/B;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,qBAAa,uBAAwB,SAAQ,cAAc;gBAC7C,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAsB9G"}
1
+ {"version":3,"file":"podfile.d.ts","sourceRoot":"","sources":["../../../../src/platforms/ios/podfile.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAIxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAO7D,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,uBAAuB,CAAA;IAC5B,SAAS,EAAE,mBAAmB,CAAA;CAC/B;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,qBAAa,uBAAwB,SAAQ,cAAc;gBAC7C,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAsB9G"}
@@ -1,4 +1,4 @@
1
1
  import type { IOSProjectDiscovery } from '../../discovery/ios';
2
- import type { NormalizedVoltraIOSConfig } from '../../config/types';
3
- export declare function resolveIOSWidgetTargetName(ios: NormalizedVoltraIOSConfig, discovery: IOSProjectDiscovery): string;
2
+ import type { ResolvedVoltraIOSConfig } from '../../config/types';
3
+ export declare function resolveIOSWidgetTargetName(ios: ResolvedVoltraIOSConfig, discovery: IOSProjectDiscovery): string;
4
4
  //# sourceMappingURL=targetName.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"targetName.d.ts","sourceRoot":"","sources":["../../../../src/platforms/ios/targetName.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAA;AAEnE,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,yBAAyB,EAAE,SAAS,EAAE,mBAAmB,GAAG,MAAM,CAOjH"}
1
+ {"version":3,"file":"targetName.d.ts","sourceRoot":"","sources":["../../../../src/platforms/ios/targetName.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAEjE,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,uBAAuB,EAAE,SAAS,EAAE,mBAAmB,GAAG,MAAM,CAO/G"}
@@ -1,13 +1,15 @@
1
1
  import { VoltraCliError } from '../../reporting/summary';
2
2
  import type { IOSProjectDiscovery } from '../../discovery/ios';
3
- import type { NormalizedVoltraIOSConfig } from '../../config/types';
3
+ import type { ResolvedVoltraIOSConfig } from '../../config/types';
4
4
  import type { ReportedChange } from '../../reporting/summary';
5
5
  export interface EnsureIOSWidgetTargetOptions {
6
6
  projectRoot: string;
7
- ios: NormalizedVoltraIOSConfig;
7
+ ios: ResolvedVoltraIOSConfig;
8
8
  discovery: IOSProjectDiscovery;
9
9
  generatedFiles: string[];
10
10
  previousGeneratedFiles?: string[];
11
+ /** User-defined build settings to write at project level, keyed by build configuration name. */
12
+ buildSettings?: Map<string, Record<string, string>>;
11
13
  }
12
14
  export interface EnsureIOSWidgetTargetResult {
13
15
  change?: ReportedChange;
@@ -1 +1 @@
1
- {"version":3,"file":"xcodeTarget.d.ts","sourceRoot":"","sources":["../../../../src/platforms/ios/xcodeTarget.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAOxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAA;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AA2D7D,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,yBAAyB,CAAA;IAC9B,SAAS,EAAE,mBAAmB,CAAA;IAC9B,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAA;CAClC;AAED,MAAM,WAAW,2BAA2B;IAC1C,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,qBAAa,4BAA6B,SAAQ,cAAc;gBAClD,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,2BAA2B,CAAC,CAkDtC"}
1
+ {"version":3,"file":"xcodeTarget.d.ts","sourceRoot":"","sources":["../../../../src/platforms/ios/xcodeTarget.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAQxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAgE7D,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,uBAAuB,CAAA;IAC5B,SAAS,EAAE,mBAAmB,CAAA;IAC9B,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAA;IACjC,gGAAgG;IAChG,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACpD;AAED,MAAM,WAAW,2BAA2B;IAC1C,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,qBAAa,4BAA6B,SAAQ,cAAc;gBAClD,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,2BAA2B,CAAC,CAwDtC"}
@@ -1,9 +1,18 @@
1
+ import { type WidgetModuleLoader } from '@use-voltra/compiler';
2
+ import type { VoltraPlatform } from '../../config/types';
1
3
  export interface WidgetModuleBuildInfo {
2
4
  isDev: false;
3
5
  metroUrl: null;
4
6
  appVersion: 'unknown';
5
- voltraVersion: '1.4.1';
7
+ voltraVersion: string;
6
8
  }
7
- export declare const DYNAMIC_WIDGET_BUILD_INFO: WidgetModuleBuildInfo;
8
- export declare function evaluateWidgetModuleExports(projectRoot: string, filePath: string, createError: (message: string) => Error): unknown;
9
+ export declare function createDynamicWidgetBuildInfo(voltraVersion: string): WidgetModuleBuildInfo;
10
+ /**
11
+ * Loader used by the apply pipeline to evaluate widget source at build time.
12
+ *
13
+ * The evaluation rules — which imports are allowed and what they resolve to — live in
14
+ * `@use-voltra/compiler` so that `voltra apply`, the Expo config plugins, and the Metro
15
+ * widget bundler all apply the same contract.
16
+ */
17
+ export declare function createGeneratedWidgetModuleLoader(projectRoot: string, platform: VoltraPlatform, createError: (message: string) => Error, onWarning?: (message: string) => void): WidgetModuleLoader;
9
18
  //# sourceMappingURL=widgetModule.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"widgetModule.d.ts","sourceRoot":"","sources":["../../../../src/platforms/shared/widgetModule.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,KAAK,CAAA;IACZ,QAAQ,EAAE,IAAI,CAAA;IACd,UAAU,EAAE,SAAS,CAAA;IACrB,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,eAAO,MAAM,yBAAyB,EAAE,qBAKvC,CAAA;AAED,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,KAAK,GACtC,OAAO,CA2CT"}
1
+ {"version":3,"file":"widgetModule.d.ts","sourceRoot":"","sources":["../../../../src/platforms/shared/widgetModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAExF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,KAAK,CAAA;IACZ,QAAQ,EAAE,IAAI,CAAA;IACd,UAAU,EAAE,SAAS,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,qBAAqB,CAOzF;AAED;;;;;;GAMG;AACH,wBAAgB,iCAAiC,CAC/C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,KAAK,EACvC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GACpC,kBAAkB,CAEpB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voltra",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "CLI for applying Voltra to native React Native projects",
5
5
  "main": "build/cjs/index.js",
6
6
  "module": "build/esm/index.js",
@@ -40,22 +40,22 @@
40
40
  "license": "MIT",
41
41
  "homepage": "https://use-voltra.dev",
42
42
  "dependencies": {
43
- "@babel/core": "^7.27.4",
44
43
  "@bacons/xcode": "^1.0.0-alpha.33",
45
44
  "@clack/prompts": "^1.0.0-alpha.5",
46
45
  "commander": "^12.1.0",
47
46
  "cosmiconfig": "^9.0.0",
48
47
  "vd-tool": "^4.0.2",
49
- "xml2js": "^0.6.2"
48
+ "xml2js": "^0.6.2",
49
+ "@use-voltra/compiler": "^2.3.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@types/babel__core": "^7.20.5"
52
+ "tsx": "^4.19.0"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "node ../../scripts/build-package.mjs packages/cli",
56
56
  "clean": "rm -rf build",
57
57
  "lint": "oxlint src",
58
58
  "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
59
- "test": "node --test"
59
+ "test": "node --import tsx --test"
60
60
  }
61
61
  }
@@ -108,7 +108,10 @@ export async function runApplyPipeline(options: ApplyOptions, dependencies: Appl
108
108
  const deletedChanges = await removeStaleGeneratedFiles(normalizedConfig.projectRoot, stateDiff.staleFiles)
109
109
  await saveVoltraState(normalizedConfig.projectRoot, { files: stateDiff.nextFiles })
110
110
 
111
- const summaryWarnings = platformResults.flatMap((result) => result.warnings ?? []).filter(isDefined)
111
+ const summaryWarnings = [
112
+ ...(normalizedConfig.warnings ?? []),
113
+ ...platformResults.flatMap((result) => result.warnings ?? []),
114
+ ].filter(isDefined)
112
115
  const summaryChanges = [...platformResults.flatMap((result) => result.changes), ...deletedChanges]
113
116
 
114
117
  await resolvedDependencies.writeSummary({ changes: summaryChanges, warnings: summaryWarnings })
@@ -0,0 +1,61 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, test } from 'node:test'
3
+
4
+ import { normalizeVoltraConfig, VoltraConfigNormalizationError } from './normalize.ts'
5
+
6
+ import type { IOSWidgetConfig } from './types.ts'
7
+
8
+ function normalizeWidgets(widgets: IOSWidgetConfig[]) {
9
+ return normalizeVoltraConfig({
10
+ config: { ios: { widgets } },
11
+ configDir: process.cwd(),
12
+ })
13
+ }
14
+
15
+ const streak: IOSWidgetConfig = { id: 'streak', displayName: 'Streak', description: 'Your daily streak' }
16
+ const weather: IOSWidgetConfig = { id: 'weather', displayName: 'Weather', description: 'Shows weather' }
17
+
18
+ describe('ios widget kind', () => {
19
+ test('accepts a custom kind', () => {
20
+ const config = normalizeWidgets([{ ...streak, kind: 'StreakWidget' }])
21
+
22
+ assert.equal(config.ios?.widgets[0]?.kind, 'StreakWidget')
23
+ })
24
+
25
+ test('leaves the kind unset when the config does not pin one', () => {
26
+ const config = normalizeWidgets([streak])
27
+
28
+ assert.equal(config.ios?.widgets[0]?.kind, undefined)
29
+ })
30
+
31
+ test('rejects an empty kind', () => {
32
+ assert.throws(
33
+ () => normalizeWidgets([{ ...streak, kind: '' }]),
34
+ (error: unknown) =>
35
+ error instanceof VoltraConfigNormalizationError && /ios\.widgets\[streak\]\.kind/.test(error.message)
36
+ )
37
+ })
38
+
39
+ test('rejects two widgets sharing a kind', () => {
40
+ assert.throws(
41
+ () =>
42
+ normalizeWidgets([
43
+ { ...streak, kind: 'Widgets' },
44
+ { ...weather, kind: 'Widgets' },
45
+ ]),
46
+ (error: unknown) =>
47
+ error instanceof VoltraConfigNormalizationError && /Duplicate ios widget kind 'Widgets'/.test(error.message)
48
+ )
49
+ })
50
+
51
+ test('rejects a custom kind that collides with another widget default kind', () => {
52
+ // `weather` would answer to `Voltra_Widget_weather` too, so both would fight over the same
53
+ // placed instances.
54
+ assert.throws(
55
+ () => normalizeWidgets([{ ...streak, kind: 'Voltra_Widget_weather' }, weather]),
56
+ (error: unknown) =>
57
+ error instanceof VoltraConfigNormalizationError &&
58
+ /Duplicate ios widget kind 'Voltra_Widget_weather'/.test(error.message)
59
+ )
60
+ })
61
+ })
@@ -2,6 +2,11 @@ import path from 'node:path'
2
2
 
3
3
  import { resolveFromRoot } from '../fs/path'
4
4
  import { CLI_DEFAULTS } from './defaults'
5
+ import { isPerConfigurationMap } from './perConfiguration'
6
+ import { resolveServerUpdateInterval, resolveServerUpdateUrl, validateServerUpdateRefresh } from './serverUpdate'
7
+ import { iosWidgetKind } from './widgetKind'
8
+
9
+ import type { PerConfiguration } from './perConfiguration'
5
10
 
6
11
  import type {
7
12
  AndroidWidgetAppIntentConfig,
@@ -10,6 +15,7 @@ import type {
10
15
  IOSWidgetConfig,
11
16
  LoadedVoltraConfig,
12
17
  NormalizedAndroidWidgetConfig,
18
+ NormalizedWidgetServerUpdateConfig,
13
19
  NormalizedVoltraAndroidConfig,
14
20
  NormalizedVoltraConfig,
15
21
  NormalizedVoltraIOSConfig,
@@ -53,6 +59,33 @@ function assertOptionalString(value: unknown, context: string): asserts value is
53
59
  }
54
60
  }
55
61
 
62
+ function assertOptionalPerConfigurationString(
63
+ value: unknown,
64
+ context: string
65
+ ): asserts value is PerConfiguration<string> | undefined {
66
+ if (value === undefined || typeof value === 'string') {
67
+ return
68
+ }
69
+
70
+ if (!isPerConfigurationMap(value as PerConfiguration<unknown>)) {
71
+ throw new VoltraConfigNormalizationError(
72
+ `${context} must be a string, or an object of strings keyed by build configuration name`
73
+ )
74
+ }
75
+
76
+ const entries = Object.entries(value as Record<string, unknown>)
77
+
78
+ if (entries.length === 0) {
79
+ throw new VoltraConfigNormalizationError(`${context} must not be an empty object`)
80
+ }
81
+
82
+ for (const [buildConfigurationName, configurationValue] of entries) {
83
+ if (typeof configurationValue !== 'string' || !configurationValue.trim()) {
84
+ throw new VoltraConfigNormalizationError(`${context}.${buildConfigurationName} must be a non-empty string`)
85
+ }
86
+ }
87
+ }
88
+
56
89
  function assertOptionalBoolean(value: unknown, context: string): asserts value is boolean | undefined {
57
90
  if (value !== undefined && typeof value !== 'boolean') {
58
91
  throw new VoltraConfigNormalizationError(`${context} must be a boolean`)
@@ -105,6 +138,26 @@ function resolveOptionalPathFromProjectRoot(projectRoot: string, filePath: strin
105
138
  return resolvePathFromProjectRoot(projectRoot, filePath)
106
139
  }
107
140
 
141
+ function resolveOptionalPerConfigurationPath(
142
+ projectRoot: string,
143
+ filePath: PerConfiguration<string> | undefined
144
+ ): PerConfiguration<string> | undefined {
145
+ if (!filePath) {
146
+ return undefined
147
+ }
148
+
149
+ if (!isPerConfigurationMap(filePath)) {
150
+ return resolvePathFromProjectRoot(projectRoot, filePath)
151
+ }
152
+
153
+ return Object.fromEntries(
154
+ Object.entries(filePath).map(([buildConfigurationName, configurationPath]) => [
155
+ buildConfigurationName,
156
+ resolvePathFromProjectRoot(projectRoot, configurationPath),
157
+ ])
158
+ )
159
+ }
160
+
108
161
  function isAbsoluteWidgetPath(value: string): boolean {
109
162
  return path.isAbsolute(value) || path.win32.isAbsolute(value) || /^[a-zA-Z]:[\\/]/.test(value)
110
163
  }
@@ -294,42 +347,68 @@ function normalizeIOSAppIntent(
294
347
  return { parameters }
295
348
  }
296
349
 
297
- function normalizeServerUpdate(
298
- serverUpdate: { url: string; intervalMinutes?: number; refresh?: boolean },
299
- context: string,
300
- defaultIntervalMinutes: number,
301
- defaultRefresh: boolean,
350
+ interface NormalizeServerUpdateOptions {
351
+ context: string
352
+ /** True when the widget has an `entry`, so the response is props rather than a payload. */
353
+ hasEntry: boolean
354
+ defaultIntervalMinutes: number
355
+ defaultRefresh: boolean
302
356
  minimumIntervalMinutes: number
303
- ): { url: string; intervalMinutes: number; refresh: boolean } {
357
+ warnings: string[]
358
+ }
359
+
360
+ function normalizeServerUpdate(
361
+ serverUpdate: { url?: string; intervalMinutes?: number; refresh?: boolean },
362
+ options: NormalizeServerUpdateOptions
363
+ ): NormalizedWidgetServerUpdateConfig {
364
+ const { context, hasEntry, defaultIntervalMinutes, defaultRefresh, minimumIntervalMinutes, warnings } = options
365
+
304
366
  assertObject(serverUpdate, context)
305
- assertNonEmptyString(serverUpdate.url, `${context}.url`)
306
367
 
307
- if (serverUpdate.intervalMinutes !== undefined) {
308
- if (typeof serverUpdate.intervalMinutes !== 'number' || !Number.isFinite(serverUpdate.intervalMinutes)) {
309
- throw new VoltraConfigNormalizationError(`${context}.intervalMinutes must be a number`)
310
- }
368
+ const url = resolveServerUpdateUrl(serverUpdate.url, context)
311
369
 
312
- if (!Number.isInteger(serverUpdate.intervalMinutes)) {
313
- throw new VoltraConfigNormalizationError(`${context}.intervalMinutes must be an integer`)
314
- }
370
+ if (url.kind === 'invalid') {
371
+ throw new VoltraConfigNormalizationError(url.error)
372
+ }
315
373
 
316
- if (serverUpdate.intervalMinutes < minimumIntervalMinutes) {
317
- throw new VoltraConfigNormalizationError(`${context}.intervalMinutes must be at least ${minimumIntervalMinutes}`)
318
- }
374
+ if (url.kind === 'insecure') {
375
+ warnings.push(url.warning)
319
376
  }
320
377
 
321
- if (serverUpdate.refresh !== undefined && typeof serverUpdate.refresh !== 'boolean') {
322
- throw new VoltraConfigNormalizationError(`${context}.refresh must be a boolean`)
378
+ const interval = resolveServerUpdateInterval({
379
+ intervalMinutes: serverUpdate.intervalMinutes,
380
+ context,
381
+ hasEntry,
382
+ defaultIntervalMinutes,
383
+ minimumIntervalMinutes,
384
+ })
385
+
386
+ if (interval.kind === 'invalid') {
387
+ throw new VoltraConfigNormalizationError(interval.error)
388
+ }
389
+
390
+ if (interval.kind === 'clamped') {
391
+ warnings.push(interval.warning)
392
+ }
393
+
394
+ const refreshError = validateServerUpdateRefresh(serverUpdate.refresh, context)
395
+
396
+ if (refreshError) {
397
+ throw new VoltraConfigNormalizationError(refreshError)
323
398
  }
324
399
 
325
400
  return {
326
401
  url: serverUpdate.url,
327
- intervalMinutes: serverUpdate.intervalMinutes ?? defaultIntervalMinutes,
402
+ intervalMinutes: interval.intervalMinutes,
328
403
  refresh: serverUpdate.refresh ?? defaultRefresh,
329
404
  }
330
405
  }
331
406
 
332
- function normalizeAndroidWidget(projectRoot: string, widget: AndroidWidgetConfig): NormalizedAndroidWidgetConfig {
407
+ function normalizeAndroidWidget(
408
+ projectRoot: string,
409
+ widget: AndroidWidgetConfig,
410
+ warnings: string[]
411
+ ): NormalizedAndroidWidgetConfig {
333
412
  assertObject(widget, 'android.widgets[]')
334
413
  assertNonEmptyString(widget.id, 'android.widgets[].id')
335
414
  assertValidWidgetId(widget.id, 'android.widgets[].id')
@@ -337,6 +416,12 @@ function normalizeAndroidWidget(projectRoot: string, widget: AndroidWidgetConfig
337
416
  assertPositiveInteger(widget.targetCellHeight, `android.widgets[${widget.id}].targetCellHeight`)
338
417
  assertOptionalPositiveInteger(widget.minCellWidth, `android.widgets[${widget.id}].minCellWidth`)
339
418
  assertOptionalPositiveInteger(widget.minCellHeight, `android.widgets[${widget.id}].minCellHeight`)
419
+ assertOptionalPositiveInteger(widget.minWidth, `android.widgets[${widget.id}].minWidth`)
420
+ assertOptionalPositiveInteger(widget.minHeight, `android.widgets[${widget.id}].minHeight`)
421
+ assertOptionalPositiveInteger(widget.minResizeWidth, `android.widgets[${widget.id}].minResizeWidth`)
422
+ assertOptionalPositiveInteger(widget.minResizeHeight, `android.widgets[${widget.id}].minResizeHeight`)
423
+ assertOptionalPositiveInteger(widget.maxResizeWidth, `android.widgets[${widget.id}].maxResizeWidth`)
424
+ assertOptionalPositiveInteger(widget.maxResizeHeight, `android.widgets[${widget.id}].maxResizeHeight`)
340
425
 
341
426
  return {
342
427
  ...widget,
@@ -352,22 +437,31 @@ function normalizeAndroidWidget(projectRoot: string, widget: AndroidWidgetConfig
352
437
  previewLayout: resolveOptionalPathFromProjectRoot(projectRoot, widget.previewLayout),
353
438
  appIntent: normalizeAndroidAppIntent(widget.appIntent, `android.widgets[${widget.id}].appIntent`),
354
439
  serverUpdate: widget.serverUpdate
355
- ? normalizeServerUpdate(
356
- widget.serverUpdate,
357
- `android.widgets[${widget.id}].serverUpdate`,
358
- CLI_DEFAULTS.android.serverUpdateIntervalMinutes,
359
- CLI_DEFAULTS.android.serverUpdateRefresh,
360
- 15
361
- )
440
+ ? normalizeServerUpdate(widget.serverUpdate, {
441
+ context: `android.widgets[${widget.id}].serverUpdate`,
442
+ hasEntry: widget.entry !== undefined,
443
+ defaultIntervalMinutes: CLI_DEFAULTS.android.serverUpdateIntervalMinutes,
444
+ defaultRefresh: CLI_DEFAULTS.android.serverUpdateRefresh,
445
+ minimumIntervalMinutes: 15,
446
+ warnings,
447
+ })
362
448
  : undefined,
363
449
  }
364
450
  }
365
451
 
366
- function normalizeIOSWidget(projectRoot: string, widget: IOSWidgetConfig): NormalizedIOSWidgetConfig {
452
+ function normalizeIOSWidget(
453
+ projectRoot: string,
454
+ widget: IOSWidgetConfig,
455
+ warnings: string[]
456
+ ): NormalizedIOSWidgetConfig {
367
457
  assertObject(widget, 'ios.widgets[]')
368
458
  assertNonEmptyString(widget.id, 'ios.widgets[].id')
369
459
  assertValidWidgetId(widget.id, 'ios.widgets[].id')
370
460
 
461
+ if (widget.kind !== undefined) {
462
+ assertNonEmptyString(widget.kind, `ios.widgets[${widget.id}].kind`)
463
+ }
464
+
371
465
  if (widget.supportedFamilies !== undefined) {
372
466
  if (!Array.isArray(widget.supportedFamilies)) {
373
467
  throw new VoltraConfigNormalizationError(`ios.widgets[${widget.id}].supportedFamilies must be an array`)
@@ -395,13 +489,14 @@ function normalizeIOSWidget(projectRoot: string, widget: IOSWidgetConfig): Norma
395
489
  ),
396
490
  appIntent: normalizeIOSAppIntent(widget.appIntent, `ios.widgets[${widget.id}].appIntent`),
397
491
  serverUpdate: widget.serverUpdate
398
- ? normalizeServerUpdate(
399
- widget.serverUpdate,
400
- `ios.widgets[${widget.id}].serverUpdate`,
401
- CLI_DEFAULTS.ios.serverUpdateIntervalMinutes,
402
- CLI_DEFAULTS.ios.serverUpdateRefresh,
403
- 1
404
- )
492
+ ? normalizeServerUpdate(widget.serverUpdate, {
493
+ context: `ios.widgets[${widget.id}].serverUpdate`,
494
+ hasEntry: widget.entry !== undefined,
495
+ defaultIntervalMinutes: CLI_DEFAULTS.ios.serverUpdateIntervalMinutes,
496
+ defaultRefresh: CLI_DEFAULTS.ios.serverUpdateRefresh,
497
+ minimumIntervalMinutes: 1,
498
+ warnings,
499
+ })
405
500
  : undefined,
406
501
  }
407
502
  }
@@ -418,6 +513,25 @@ function assertUniqueWidgetIds(widgetIds: string[], context: string): void {
418
513
  }
419
514
  }
420
515
 
516
+ /**
517
+ * WidgetKit identifies a placed widget by extension bundle id + kind, so two widgets sharing a kind
518
+ * would fight over the same Home Screen instances. Compared after defaulting, which also catches a
519
+ * custom kind that collides with another widget's `Voltra_Widget_<id>`.
520
+ */
521
+ function assertUniqueIOSWidgetKinds(widgets: Pick<IOSWidgetConfig, 'id' | 'kind'>[]): void {
522
+ const seen = new Set<string>()
523
+
524
+ for (const widget of widgets) {
525
+ const kind = iosWidgetKind(widget)
526
+
527
+ if (seen.has(kind)) {
528
+ throw new VoltraConfigNormalizationError(`Duplicate ios widget kind '${kind}'`)
529
+ }
530
+
531
+ seen.add(kind)
532
+ }
533
+ }
534
+
421
535
  function assertValidWidgetId(widgetId: string, context: string): void {
422
536
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(widgetId)) {
423
537
  throw new VoltraConfigNormalizationError(
@@ -435,6 +549,7 @@ function assertValidIOSTargetName(targetName: string, context: string): void {
435
549
  }
436
550
 
437
551
  function normalizeAndroidConfig(
552
+ warnings: string[],
438
553
  projectRoot: string,
439
554
  config: LoadedVoltraConfig['config']['android']
440
555
  ): NormalizedVoltraAndroidConfig | undefined {
@@ -459,7 +574,7 @@ function normalizeAndroidConfig(
459
574
  throw new VoltraConfigNormalizationError('android.widgets must be an array')
460
575
  }
461
576
 
462
- const widgets = (config.widgets ?? []).map((widget) => normalizeAndroidWidget(projectRoot, widget))
577
+ const widgets = (config.widgets ?? []).map((widget) => normalizeAndroidWidget(projectRoot, widget, warnings))
463
578
  assertUniqueWidgetIds(
464
579
  widgets.map((widget) => widget.id),
465
580
  'android'
@@ -483,6 +598,7 @@ function normalizeAndroidConfig(
483
598
  }
484
599
 
485
600
  function normalizeIOSConfig(
601
+ warnings: string[],
486
602
  projectRoot: string,
487
603
  config: LoadedVoltraConfig['config']['ios']
488
604
  ): NormalizedVoltraIOSConfig | undefined {
@@ -492,12 +608,12 @@ function normalizeIOSConfig(
492
608
 
493
609
  assertObject(config, 'ios')
494
610
  assertOptionalBoolean(config.enablePushNotifications, 'ios.enablePushNotifications')
495
- assertOptionalString(config.groupIdentifier, 'ios.groupIdentifier')
611
+ assertOptionalPerConfigurationString(config.groupIdentifier, 'ios.groupIdentifier')
496
612
  assertOptionalString(config.deploymentTarget, 'ios.deploymentTarget')
497
613
  assertOptionalString(config.targetName, 'ios.targetName')
498
614
  assertOptionalStringArray(config.fonts, 'ios.fonts')
499
615
  assertOptionalString(config.userImagesPath, 'ios.userImagesPath')
500
- assertOptionalString(config.keychainGroup, 'ios.keychainGroup')
616
+ assertOptionalPerConfigurationString(config.keychainGroup, 'ios.keychainGroup')
501
617
 
502
618
  if (config.project !== undefined) {
503
619
  assertObject(config.project, 'ios.project')
@@ -505,7 +621,7 @@ function normalizeIOSConfig(
505
621
  assertOptionalString(config.project.xcodeprojPath, 'ios.project.xcodeprojPath')
506
622
  assertOptionalString(config.project.mainTargetName, 'ios.project.mainTargetName')
507
623
  assertOptionalString(config.project.infoPlistPath, 'ios.project.infoPlistPath')
508
- assertOptionalString(config.project.entitlementsPath, 'ios.project.entitlementsPath')
624
+ assertOptionalPerConfigurationString(config.project.entitlementsPath, 'ios.project.entitlementsPath')
509
625
  assertOptionalString(config.project.podfilePath, 'ios.project.podfilePath')
510
626
  }
511
627
 
@@ -517,11 +633,12 @@ function normalizeIOSConfig(
517
633
  assertValidIOSTargetName(config.targetName, 'ios.targetName')
518
634
  }
519
635
 
520
- const widgets = (config.widgets ?? []).map((widget) => normalizeIOSWidget(projectRoot, widget))
636
+ const widgets = (config.widgets ?? []).map((widget) => normalizeIOSWidget(projectRoot, widget, warnings))
521
637
  assertUniqueWidgetIds(
522
638
  widgets.map((widget) => widget.id),
523
639
  'ios'
524
640
  )
641
+ assertUniqueIOSWidgetKinds(widgets)
525
642
 
526
643
  return {
527
644
  enablePushNotifications: config.enablePushNotifications ?? CLI_DEFAULTS.ios.enablePushNotifications,
@@ -537,7 +654,7 @@ function normalizeIOSConfig(
537
654
  xcodeprojPath: resolveOptionalPathFromProjectRoot(projectRoot, config.project?.xcodeprojPath),
538
655
  mainTargetName: config.project?.mainTargetName,
539
656
  infoPlistPath: resolveOptionalPathFromProjectRoot(projectRoot, config.project?.infoPlistPath),
540
- entitlementsPath: resolveOptionalPathFromProjectRoot(projectRoot, config.project?.entitlementsPath),
657
+ entitlementsPath: resolveOptionalPerConfigurationPath(projectRoot, config.project?.entitlementsPath),
541
658
  podfilePath: resolveOptionalPathFromProjectRoot(projectRoot, config.project?.podfilePath),
542
659
  },
543
660
  }
@@ -552,11 +669,38 @@ export function normalizeVoltraConfig(loadedConfig: LoadedVoltraConfig): Normali
552
669
  loadedConfig.config.projectRoot ?? loadedConfig.configDir
553
670
  )
554
671
 
672
+ const warnings: string[] = []
673
+ const android = normalizeAndroidConfig(warnings, projectRoot, loadedConfig.config.android)
674
+ const ios = normalizeIOSConfig(warnings, projectRoot, loadedConfig.config.ios)
675
+
676
+ assertServerDrivenDynamicWidgetsAreSupported(ios)
677
+
555
678
  return {
556
679
  configPath: loadedConfig.configPath,
557
680
  configDir: loadedConfig.configDir,
558
681
  projectRoot,
559
- android: normalizeAndroidConfig(projectRoot, loadedConfig.config.android),
560
- ios: normalizeIOSConfig(projectRoot, loadedConfig.config.ios),
682
+ android,
683
+ ios,
684
+ warnings,
685
+ }
686
+ }
687
+
688
+ /**
689
+ * A Dynamic Widget commits fetched props to the App Group so the widget extension can read them,
690
+ * so a server-driven one without a `groupIdentifier` would fetch and have nowhere to put the
691
+ * result.
692
+ */
693
+ function assertServerDrivenDynamicWidgetsAreSupported(ios: NormalizedVoltraIOSConfig | undefined): void {
694
+ if (ios === undefined || ios.groupIdentifier !== undefined) {
695
+ return
696
+ }
697
+
698
+ for (const widget of ios.widgets) {
699
+ if (widget.entry !== undefined && widget.serverUpdate !== undefined) {
700
+ throw new VoltraConfigNormalizationError(
701
+ `ios.widgets[${widget.id}] has both entry and serverUpdate, which requires ios.groupIdentifier ` +
702
+ 'so fetched props can be shared with the widget extension.'
703
+ )
704
+ }
561
705
  }
562
706
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A value that is either shared by every Xcode build configuration, or given once per build
3
+ * configuration name for apps that ship several environments from the same project.
4
+ *
5
+ * ```ts
6
+ * groupIdentifier: 'group.com.example.app'
7
+ * groupIdentifier: { Debug: 'group.com.example.app.dev', Release: 'group.com.example.app' }
8
+ * ```
9
+ */
10
+ export type PerConfiguration<TValue> = TValue | Record<string, TValue>
11
+
12
+ export function isPerConfigurationMap<TValue>(
13
+ value: PerConfiguration<TValue> | undefined
14
+ ): value is Record<string, TValue> {
15
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
16
+ }
17
+
18
+ /**
19
+ * Pairs a per-build-configuration value with the project's real build configurations, in the
20
+ * project's own order. Keys that name no build configuration, and build configurations the value
21
+ * does not cover, are reported before anything is written to the project.
22
+ */
23
+ export function getValuesByBuildConfiguration<TValue>(
24
+ value: Record<string, TValue>,
25
+ buildConfigurationNames: string[],
26
+ context: string,
27
+ createError: (message: string) => Error
28
+ ): Map<string, TValue> {
29
+ const knownNames = `This project has: ${buildConfigurationNames.join(', ')}.`
30
+ const unknownNames = Object.keys(value).filter((name) => !buildConfigurationNames.includes(name))
31
+
32
+ if (unknownNames.length > 0) {
33
+ throw createError(`${context} names unknown build configurations: ${unknownNames.join(', ')}. ${knownNames}`)
34
+ }
35
+
36
+ const valuesByBuildConfiguration = new Map<string, TValue>()
37
+
38
+ for (const buildConfigurationName of buildConfigurationNames) {
39
+ const configurationValue = value[buildConfigurationName]
40
+
41
+ if (configurationValue === undefined) {
42
+ throw createError(`${context} has no value for build configuration '${buildConfigurationName}'. ${knownNames}`)
43
+ }
44
+
45
+ valuesByBuildConfiguration.set(buildConfigurationName, configurationValue)
46
+ }
47
+
48
+ return valuesByBuildConfiguration
49
+ }