@bacons/apple-targets 0.0.5 → 0.0.7

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/README.md CHANGED
@@ -3,12 +3,10 @@
3
3
  > [!WARNING]
4
4
  > This is highly experimental and not part of any official Expo workflow.
5
5
 
6
-
7
6
  <img width="1061" alt="Screenshot 2023-06-10 at 1 59 26 PM" src="https://github.com/EvanBacon/expo-apple-targets/assets/9664363/4cd8399d-53aa-401a-9caa-3a1432a0640c">
8
7
 
9
8
  An experimental Expo Config Plugin that generates native Apple Targets like Widgets or App Clips, and links them outside the `/ios` directory. You can open Xcode and develop the targets inside the virtual `expo:targets` folder and the changes will be saved outside of the `ios` directory. This pattern enables building things that fall outside of the scope of React Native while still obtaining all the benefits of Continuous Native Generation.
10
9
 
11
-
12
10
  ## 🚀 How to use
13
11
 
14
12
  - Add targets to `targets/` directory with an `expo-target.config.json` file.
@@ -95,6 +93,41 @@ There are certain values that are shared across targets. We use a predefined con
95
93
  | `$accent` | `ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME` | Sets the global accent color, in widgets this is used for the tint color of buttons when editing the widget. |
96
94
  | `$widgetBackground` | `ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME` | Sets the background color of the widget. |
97
95
 
96
+ ## CocoaPods
97
+
98
+ Adding a file `pods.rb` in the root of the repo will enable you to modify the target settings for the project.
99
+
100
+ The ruby module evaluates with global access to the property `podfile_properties` and the method `use_native_modules`.
101
+
102
+ For example, the following is useful for enabling React Native in an App Clip target:
103
+
104
+ ```rb
105
+ exclude = []
106
+ use_expo_modules!(exclude: exclude)
107
+ config = use_native_modules!
108
+
109
+ use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
110
+ use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
111
+
112
+ use_react_native!(
113
+ :path => config[:reactNativePath],
114
+ :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
115
+ # An absolute path to your application root.
116
+ :app_path => "#{Pod::Config.instance.installation_root}/..",
117
+ :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
118
+ )
119
+ ```
120
+
121
+ This block executes at the end of the Podfile in a block like:
122
+
123
+ ```rb
124
+ target "target_dir_name" do
125
+ target_file
126
+ end
127
+ ```
128
+
129
+ The name of the target must match the name of the target directory.
130
+
98
131
  ## Examples
99
132
 
100
133
  ### `widget`
@@ -5,9 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.customColorFromCSS = void 0;
7
7
  // @ts-expect-error
8
- const normalize_color_1 = __importDefault(require("@react-native/normalize-color"));
8
+ const normalize_colors_1 = __importDefault(require("@react-native/normalize-colors"));
9
9
  function customColorFromCSS(color) {
10
- let colorInt = (0, normalize_color_1.default)(color);
10
+ let colorInt = (0, normalize_colors_1.default)(color);
11
11
  colorInt = ((colorInt << 24) | (colorInt >>> 8)) >>> 0;
12
12
  const red = ((colorInt >> 16) & 255) / 255;
13
13
  const green = ((colorInt >> 8) & 255) / 255;
package/build/config.d.ts CHANGED
@@ -49,6 +49,7 @@ export type Entitlements = Partial<{
49
49
  "com.apple.developer.driverkit.allow-third-party-userclients": boolean;
50
50
  "com.apple.developer.weatherkit": boolean;
51
51
  "com.apple.developer.on-demand-install-capable": boolean;
52
+ "com.apple.developer.parent-application-identifiers": string[];
52
53
  "com.apple.developer.driverkit.family.scsicontroller": boolean;
53
54
  "com.apple.developer.driverkit.family.serial": boolean;
54
55
  "com.apple.developer.driverkit.family.networking": boolean;
@@ -100,4 +101,6 @@ export type Config = {
100
101
  "2x"?: string;
101
102
  "3x"?: string;
102
103
  }>;
104
+ /** Should the release build export the JS bundle and embed. Intended for App Clips and Share Extensions where you may want to use React Native. */
105
+ exportJs?: boolean;
103
106
  };
@@ -238,8 +238,8 @@ async function generateWatchIconsInternalAsync(icon, projectRoot, iosNamedProjec
238
238
  imagesJson.push({
239
239
  filename: getAppleIconName(size, 1),
240
240
  idiom: "universal",
241
- platform: "watchos",
242
241
  size: `${size}x${size}`,
242
+ platform: "watchos",
243
243
  });
244
244
  return imagesJson;
245
245
  }
@@ -199,8 +199,8 @@ async function generateWatchIconsInternalAsync(icon, projectRoot, iosNamedProjec
199
199
  imagesJson.push({
200
200
  filename: getAppleIconName(size, 1),
201
201
  idiom: "universal",
202
- platform: "watchos",
203
202
  size: `${size}x${size}`,
203
+ platform: "watchos",
204
204
  });
205
205
  return imagesJson;
206
206
  }
package/build/index.js CHANGED
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.withTargetsDir = void 0;
7
7
  const glob_1 = require("glob");
8
8
  const path_1 = __importDefault(require("path"));
9
+ const withPodTargetExtension_1 = require("./withPodTargetExtension");
9
10
  const withWidget_1 = __importDefault(require("./withWidget"));
10
11
  const withXcparse_1 = require("./withXcparse");
11
12
  const withTargetsDir = (config, { appleTeamId, root = "./targets", match = "*" }) => {
@@ -22,6 +23,7 @@ const withTargetsDir = (config, { appleTeamId, root = "./targets", match = "*" }
22
23
  directory: path_1.default.relative(projectRoot, path_1.default.dirname(configPath)),
23
24
  });
24
25
  });
26
+ (0, withPodTargetExtension_1.withPodTargetExtension)(config);
25
27
  return (0, withXcparse_1.withXcodeProjectBetaBaseMod)(config);
26
28
  };
27
29
  exports.withTargetsDir = withTargetsDir;
@@ -0,0 +1,3 @@
1
+ import { ConfigPlugin } from "expo/config-plugins";
2
+ /** Inject a helper which matches `pods.rb` files in the target root directory and invokes it as a way to extend the Podfile. */
3
+ export declare const withPodTargetExtension: ConfigPlugin;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withPodTargetExtension = void 0;
4
+ const config_plugins_1 = require("expo/config-plugins");
5
+ // TODO: This won't always match the correct target name. Need to pull the same algo in.
6
+ const extension = `# Dynamic loading of target configurations
7
+ Dir.glob(File.join(__dir__, '..', 'targets', '**', 'pods.rb')).each do |target_file|
8
+ target_name = File.basename(File.dirname(target_file))
9
+ target target_name do
10
+ # Create a new binding with access to necessary methods and variables
11
+ target_binding = binding
12
+ target_binding.local_variable_set(:podfile_properties, podfile_properties)
13
+ target_binding.local_variable_set(:config, use_native_modules!)
14
+
15
+ # Evaluate the target file content in the new binding
16
+ eval(File.read(target_file), target_binding, target_file)
17
+ end
18
+ end
19
+ `;
20
+ /** Inject a helper which matches `pods.rb` files in the target root directory and invokes it as a way to extend the Podfile. */
21
+ const withPodTargetExtension = (config) => (0, config_plugins_1.withPodfile)(config, (config) => {
22
+ if (config.modResults.contents.includes("apple-targets-extension-loader")) {
23
+ return config;
24
+ }
25
+ config.modResults.contents += "\n\n" + extension;
26
+ return config;
27
+ });
28
+ exports.withPodTargetExtension = withPodTargetExtension;
@@ -22,7 +22,7 @@ function kebabToCamelCase(str) {
22
22
  }
23
23
  const withWidget = (config, props) => {
24
24
  // TODO: Magically based on the top-level folders in the `ios-widgets/` folder
25
- var _a, _b, _c, _d, _e, _f;
25
+ var _a, _b, _c, _d, _e, _f, _g;
26
26
  if (props.icon && !/https?:\/\//.test(props.icon)) {
27
27
  props.icon = path_1.default.join(props.directory, props.icon);
28
28
  }
@@ -40,8 +40,22 @@ const withWidget = (config, props) => {
40
40
  throw new Error(`Found multiple entitlements files in ${widgetFolderAbsolutePath}`);
41
41
  }
42
42
  let entitlementsJson = props.entitlements;
43
+ // Apply default entitlements that must be present for a target to work.
44
+ function applyDefaultEntitlements(entitlements) {
45
+ if (props.type === "clip") {
46
+ entitlements["com.apple.developer.parent-application-identifiers"] = [
47
+ `$(AppIdentifierPrefix)${config.ios.bundleIdentifier}`,
48
+ ];
49
+ // NOTE: This doesn't seem to be required anymore (Oct 12 2024):
50
+ // entitlements["com.apple.developer.on-demand-install-capable"] = true;
51
+ }
52
+ return entitlements;
53
+ }
54
+ if (entitlementsJson) {
55
+ entitlementsJson = applyDefaultEntitlements(entitlementsJson);
56
+ }
43
57
  // If the user defined entitlements, then overwrite any existing entitlements file
44
- if (props.entitlements) {
58
+ if (entitlementsJson) {
45
59
  (0, config_plugins_1.withDangerousMod)(config, [
46
60
  "ios",
47
61
  async (config) => {
@@ -52,7 +66,7 @@ const withWidget = (config, props) => {
52
66
  if (entitlementsFiles[0]) {
53
67
  console.log(`[${widget}] Replacing ${path_1.default.relative(widgetFolderAbsolutePath, entitlementsFiles[0])} with entitlements JSON from config`);
54
68
  }
55
- fs_1.default.writeFileSync(entitlementsFilePath, plist_1.default.build(props.entitlements));
69
+ fs_1.default.writeFileSync(entitlementsFilePath, plist_1.default.build(entitlementsJson));
56
70
  return config;
57
71
  },
58
72
  ]);
@@ -111,6 +125,9 @@ const withWidget = (config, props) => {
111
125
  frameworks: (0, target_1.getFrameworksForType)(props.type).concat(props.frameworks || []),
112
126
  type: props.type,
113
127
  teamId: props.appleTeamId,
128
+ exportJs: (_g = props.exportJs) !== null && _g !== void 0 ? _g :
129
+ // Assume App Clips are used for React Native.
130
+ props.type === "clip",
114
131
  });
115
132
  config = (0, withEasCredentials_1.withEASTargets)(config, {
116
133
  targetName,
@@ -13,5 +13,6 @@ export type XcodeSettings = {
13
13
  colors?: Record<string, string>;
14
14
  teamId?: string;
15
15
  icon?: string;
16
+ exportJs?: boolean;
16
17
  };
17
18
  export declare const withXcodeChanges: ConfigPlugin<XcodeSettings>;
@@ -687,6 +687,36 @@ async function applyXcodeChanges(config, project, props) {
687
687
  }
688
688
  return assets;
689
689
  }
690
+ function configureJsExport(target) {
691
+ if (props.exportJs) {
692
+ const shellScript = mainAppTarget.props.buildPhases.find((phase) => xcode_1.PBXShellScriptBuildPhase.is(phase) &&
693
+ phase.props.name === "Bundle React Native code and images");
694
+ if (!shellScript) {
695
+ throw new Error('Failed to find the "Bundle React Native code and images" build phase in the main app target.');
696
+ }
697
+ const currentShellScript = target.props.buildPhases.find((phase) => xcode_1.PBXShellScriptBuildPhase.is(phase) &&
698
+ phase.props.name === "Bundle React Native code and images");
699
+ if (!currentShellScript) {
700
+ target.createBuildPhase(xcode_1.PBXShellScriptBuildPhase, {
701
+ ...shellScript.props,
702
+ });
703
+ }
704
+ else {
705
+ for (const key in shellScript.props) {
706
+ // @ts-expect-error
707
+ currentShellScript.props[key] = shellScript.props[key];
708
+ }
709
+ }
710
+ }
711
+ else {
712
+ // Remove the shell script build phase if it exists from a subsequent build.
713
+ const shellScript = target.props.buildPhases.findIndex((phase) => xcode_1.PBXShellScriptBuildPhase.is(phase) &&
714
+ phase.props.name === "Bundle React Native code and images");
715
+ if (shellScript !== -1) {
716
+ target.props.buildPhases.splice(shellScript, 1);
717
+ }
718
+ }
719
+ }
690
720
  if (targetToUpdate) {
691
721
  // Remove existing build phases
692
722
  targetToUpdate.props.buildConfigurationList.props.buildConfigurations.forEach((config) => {
@@ -708,6 +738,7 @@ async function applyXcodeChanges(config, project, props) {
708
738
  configureTargetWithEntitlements(targetToUpdate);
709
739
  configureTargetWithPreview(targetToUpdate);
710
740
  configureTargetWithKnownSettings(targetToUpdate);
741
+ configureJsExport(targetToUpdate);
711
742
  applyDevelopmentTeamIdToTargets();
712
743
  syncMarketingVersions();
713
744
  return project;
@@ -760,6 +791,34 @@ async function applyXcodeChanges(config, project, props) {
760
791
  }))
761
792
  .flat();
762
793
  const resAssets = [];
794
+ // Support for LaunchScreen files
795
+ const baseProj = path_1.default.join(magicCwd, "Base.lproj");
796
+ if (fs_1.default.existsSync(baseProj)) {
797
+ // Link LaunchScreen.storyboard
798
+ fs_1.default.readdirSync(baseProj).forEach((file) => {
799
+ if (file === ".DS_Store")
800
+ return;
801
+ const stat = fs_1.default.statSync(path_1.default.join(baseProj, file));
802
+ if (stat.isFile()) {
803
+ if (file.endsWith(".storyboard")) {
804
+ assetFiles.push(xcode_1.PBXBuildFile.create(project, {
805
+ fileRef: xcode_1.PBXVariantGroup.create(project, {
806
+ name: file,
807
+ sourceTree: "<group>",
808
+ children: [
809
+ xcode_1.PBXFileReference.create(project, {
810
+ lastKnownFileType: "file.storyboard",
811
+ name: "Base",
812
+ path: path_1.default.join("Base.lproj", file),
813
+ sourceTree: "<group>",
814
+ }),
815
+ ],
816
+ }),
817
+ }));
818
+ }
819
+ }
820
+ });
821
+ }
763
822
  // TODO: Maybe just limit this to Safari?
764
823
  if (fs_1.default.existsSync(path_1.default.join(magicCwd, "assets"))) {
765
824
  // get top-level directories in `assets/` and append them to assetFiles as folder types
@@ -838,6 +897,7 @@ async function applyXcodeChanges(config, project, props) {
838
897
  widgetTarget.createBuildPhase(xcode_1.PBXResourcesBuildPhase, {
839
898
  files: [...assetFiles, ...resAssets],
840
899
  });
900
+ configureJsExport(widgetTarget);
841
901
  const containerItemProxy = xcode_1.PBXContainerItemProxy.create(project, {
842
902
  containerPortal: project.rootObject,
843
903
  proxyType: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacons/apple-targets",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Generate Apple Targets with Expo Prebuild",
5
5
  "main": "build/index.js",
6
6
  "files": [
@@ -28,7 +28,7 @@
28
28
  "author": "Evan Bacon",
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
- "@react-native/normalize-color": "^2.1.0",
31
+ "@react-native/normalize-colors": "^0.75.4",
32
32
  "glob": "^10.2.6",
33
33
  "@bacons/xcode": "^1.0.0-alpha.13",
34
34
  "fs-extra": "^11.2.0"