@bacons/apple-targets 0.0.4 → 0.0.6
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 +35 -2
- package/build/colorset/customColorFromCSS.js +2 -2
- package/build/config.d.ts +3 -0
- package/build/icon/withImageAsset.js +1 -1
- package/build/icon/withIosIcon.js +1 -1
- package/build/index.js +2 -0
- package/build/withPodTargetExtension.d.ts +3 -0
- package/build/withPodTargetExtension.js +34 -0
- package/build/withWidget.js +20 -3
- package/build/withXcodeChanges.d.ts +1 -0
- package/build/withXcodeChanges.js +60 -0
- package/package.json +2 -1
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
|
|
8
|
+
const normalize_colors_1 = __importDefault(require("@react-native/normalize-colors"));
|
|
9
9
|
function customColorFromCSS(color) {
|
|
10
|
-
let colorInt = (0,
|
|
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,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withPodTargetExtension = void 0;
|
|
4
|
+
const generateCode_1 = require("@expo/config-plugins/build/utils/generateCode");
|
|
5
|
+
const config_plugins_1 = require("expo/config-plugins");
|
|
6
|
+
// TODO: This won't always match the correct target name. Need to pull the same algo in.
|
|
7
|
+
const extension = `# Dynamic loading of target configurations
|
|
8
|
+
Dir.glob(File.join(__dir__, '..', 'targets', '**', 'pods.rb')).each do |target_file|
|
|
9
|
+
target_name = File.basename(File.dirname(target_file))
|
|
10
|
+
target target_name do
|
|
11
|
+
# Create a new binding with access to necessary methods and variables
|
|
12
|
+
target_binding = binding
|
|
13
|
+
target_binding.local_variable_set(:podfile_properties, podfile_properties)
|
|
14
|
+
target_binding.local_variable_set(:config, use_native_modules!)
|
|
15
|
+
|
|
16
|
+
# Evaluate the target file content in the new binding
|
|
17
|
+
eval(File.read(target_file), target_binding, target_file)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
`;
|
|
21
|
+
/** Inject a helper which matches `pods.rb` files in the target root directory and invokes it as a way to extend the Podfile. */
|
|
22
|
+
const withPodTargetExtension = (config) => (0, config_plugins_1.withPodfile)(config, (config) => {
|
|
23
|
+
config.modResults.contents = (0, generateCode_1.mergeContents)({
|
|
24
|
+
tag: "apple-targets-extension-loader",
|
|
25
|
+
src: config.modResults.contents,
|
|
26
|
+
newSrc: extension,
|
|
27
|
+
// Add at the end of the file.
|
|
28
|
+
anchor: /Pod::UI\.warn e/,
|
|
29
|
+
offset: 4,
|
|
30
|
+
comment: "#",
|
|
31
|
+
}).contents;
|
|
32
|
+
return config;
|
|
33
|
+
});
|
|
34
|
+
exports.withPodTargetExtension = withPodTargetExtension;
|
package/build/withWidget.js
CHANGED
|
@@ -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 (
|
|
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(
|
|
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,
|
|
@@ -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.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Generate Apple Targets with Expo Prebuild",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"files": [
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"author": "Evan Bacon",
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@react-native/normalize-colors": "^0.75.4",
|
|
31
32
|
"glob": "^10.2.6",
|
|
32
33
|
"@bacons/xcode": "^1.0.0-alpha.13",
|
|
33
34
|
"fs-extra": "^11.2.0"
|