@bacons/apple-targets 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 evanbacon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # Apple Targets plugin
2
+
3
+ <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">
4
+
5
+ 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.
6
+
7
+ > This is highly experimental and not part of any official Expo workflow.
8
+
9
+ ## 🚀 How to use
10
+
11
+ - Add targets to `targets/` directory with an `expo-target.config.json` file.
12
+ - Currently, if you don't have an `Info.plist`, it'll be generated on `npx expo prebuild`. This may be changed in the future so if you have an `Info.plist` it'll be used, otherwise, it'll be generated.
13
+ - Any files in a top-level `target/*/assets` directory will be linked as resources of the target. This was added to support Safari Extensions.
14
+ - A single top-level `*.entitlements` file will be linked as the entitlements of the target. This is not currently used in EAS Capability signing, but may be in the future.
15
+ - All top-level swift files will be linked as build sources of the target. There is currently no support for storyboard or `.xib` files because I can't be bothered.
16
+ - All top-level `*.xcassets` will be linked as resources, and accessible in the targets. If you add files outside of Xcode, you'll need to re-run `npx expo prebuild` to link them.
17
+ - Code-signing requires the teamId be provided to the plugin in `app.config.js`.
18
+
19
+ ```json
20
+ {
21
+ "plugins": [
22
+ [
23
+ "@bacons/apple-targets",
24
+ {
25
+ "teamId": "XXXXXXXXXX"
26
+ }
27
+ ]
28
+ ]
29
+ }
30
+ ```
31
+
32
+ You can change the root directory from `./targets` to something else with `root: "./src/targets"`. Avoid doing this.
33
+
34
+ ## Using React Native in Targets
35
+
36
+ I'm not sure, that's not the purpose of this plugin. I built this so I could easily build iOS widgets and other minor targets with SwiftUI. I imagine it would be straightforward to use React Native in share, notification, iMessage, Safari, and photo editing extensions, you can build that on top of this plugin if you want.
37
+
38
+ ## `expo-target.config.json`
39
+
40
+ This file can have the following properties:
41
+
42
+ ```json
43
+ {
44
+ "type": "widget",
45
+
46
+ // Name of the target/product. Defaults to the directory name.
47
+ "name": "My Widget",
48
+
49
+ // Generates colorset files for the target.
50
+ "colors": {
51
+ // or "$accent": "red",
52
+ "$accent": { "color": "red", "darkColor": "blue" }
53
+ },
54
+ "icon": "../assets/icon.png",
55
+ // Can also be a URL
56
+ "frameworks": [
57
+ // Frameworks without the extension, these will be added to the target.
58
+ "SwiftUI"
59
+ ],
60
+ "entitlements": {
61
+ // Serialized entitlements. Useful for configuring with environment variables.
62
+ },
63
+ // Generates xcassets for the target.
64
+ "images": {
65
+ "thing": "../assets/thing.png"
66
+ },
67
+
68
+ // The iOS version fot the target.
69
+ "deploymentTarget": "13.4"
70
+ }
71
+ ```
72
+
73
+ You can also use `.js` with the typedoc for autocomplete:
74
+
75
+ ```js
76
+ /** @type {import('@bacons/apple-targets').Config} */
77
+ module.exports = {
78
+ type: "watch",
79
+ colors: {
80
+ $accent: "steelblue",
81
+ },
82
+ deploymentTarget: "9.4",
83
+ };
84
+ ```
85
+
86
+ ## Colors
87
+
88
+ There are certain values that are shared across targets. We use a predefined convention to map these values across targets.
89
+
90
+ | Name | Build Setting | Purpose |
91
+ | ------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
92
+ | `$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. |
93
+ | `$widgetBackground` | `ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME` | Sets the background color of the widget. |
94
+
95
+ ## Examples
96
+
97
+ ### `widget`
98
+
99
+ > I wrote a blog about this one and used it in production. Learn more: [Expo x Apple Widgets](https://evanbacon.dev/blog/apple-home-screen-widgets).
100
+
101
+ ```js
102
+ /** @type {import('@bacons/apple-targets').Config} */
103
+ module.exports = {
104
+ type: "widget",
105
+ icon: "../../icons/widget.png",
106
+ colors: {
107
+ // This color is referenced in the Info.plist
108
+ $widgetBackground: "#DB739C",
109
+
110
+ $accent: "#F09458",
111
+
112
+ // Optional: Add colors that can be used in SwiftUI.
113
+ gradient1: {
114
+ light: "#E4975D",
115
+ dark: "#3E72A0",
116
+ },
117
+ },
118
+ // Optional: Add images that can be used in SwiftUI.
119
+ images: {
120
+ valleys: "../../valleys.png",
121
+ },
122
+ // Optional: Add entitlements to the target, this one can be used to share data between the widget and the app.
123
+ entitlements: {
124
+ "com.apple.security.application-groups": ["group.bacon.data"],
125
+ },
126
+ };
127
+ ```
128
+
129
+ ### `action`
130
+
131
+ These show up in the share sheet. The icon should be transparent as it will be masked by the system.
132
+
133
+ ```js
134
+ /** @type {import('@bacons/apple-targets').Config} */
135
+ module.exports = {
136
+ type: "action",
137
+ name: "Inspect Element",
138
+ icon: "./assets/icon.png",
139
+ colors: {
140
+ TouchBarBezel: "#DB739C",
141
+ },
142
+ };
143
+ ```
144
+
145
+ Add a JavaScript file to `assets/index.js`:
146
+
147
+ ```js
148
+ class Action {
149
+ /**
150
+ * `extensionName: "com.bacon.2095.axun"`
151
+ * @param {*} arguments: {completionFunction: () => unknown; extensionName: string; }
152
+ */
153
+ run({ extensionName, completionFunction }) {
154
+ // Here, you can run code that modifies the document and/or prepares
155
+ // things to pass to your action's native code.
156
+
157
+ // We will not modify anything, but will pass the body's background
158
+ // style to the native code.
159
+ completionFunction({
160
+ /* */
161
+ });
162
+ }
163
+
164
+ finalize() {
165
+ // Runs after the native action code has completed.
166
+ }
167
+ }
168
+
169
+ window.ExtensionPreprocessingJS = new Action();
170
+ ```
171
+
172
+ Ensure `NSExtensionJavaScriptPreprocessingFile: "index"` in the Info.plist.
173
+
174
+ ### `spotlight`
175
+
176
+ Populate the Spotlight search results with your app's content.
177
+
178
+ ```js
179
+ /** @type {import('@bacons/apple-targets').Config} */
180
+ module.exports = {
181
+ type: "spotlight",
182
+ };
183
+ ```
184
+
185
+ ### Supported types
186
+
187
+ Ideally, this would be generated automatically based on a fully qualified Xcode project, but for now it's a manual process. The currently supported types are based on static analysis of the most commonly used targets in the iOS App Store. I haven't tested all of these and they may not work.
188
+
189
+ | Type | Description |
190
+ | -------------------- | ---------------------------------- |
191
+ | action | Share Action |
192
+ | widget | Widget / Live Activity |
193
+ | watch | Watch App (with companion iOS App) |
194
+ | clip | App Clip |
195
+ | safari | Safari Extension |
196
+ | share | Share Extension |
197
+ | notification-content | Notification Content Extension |
198
+ | notification-service | Notification Service Extension |
199
+ | intent | Siri Intent Extension |
200
+ | intent-ui | Siri Intent UI Extension |
201
+ | spotlight | Spotlight Index Extension |
202
+ | bg-download | Background Download Extension |
203
+ | quicklook-thumbnail | Quick Look Thumbnail Extension |
204
+ | location-push | Location Push Service Extension |
205
+ | credentials-provider | Credentials Provider Extension |
206
+ | account-auth | Account Authentication Extension |
207
+
208
+ <!-- | imessage | iMessage Extension | -->
209
+
210
+ ## Code Signing
211
+
212
+ The codesigning is theoretically handled entirely by [EAS Build](https://docs.expo.dev/build/introduction/). This plugin will add the requisite entitlements for target signing to work. I've only tested this end-to-end with my Pillar Valley Widget.
213
+
214
+ You can also manually sign all sub-targets if you want, I'll light a candle for you.
215
+
216
+ ## Xcode parsing
217
+
218
+ This plugin makes use of my proprietary Xcode parsing library, [`@bacons/xcode`](https://github.com/evanbacon/xcode). It's mostly typed, very untested, and possibly full of bugs––however, it's still 10x nicer than the alternative.
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./build')
@@ -0,0 +1,10 @@
1
+ export declare function customColorFromCSS(color: string): {
2
+ /** @example `0.86584504117670746` */
3
+ red: number;
4
+ /** @example `0.26445041990630447` */
5
+ green: number;
6
+ /** @example `0.3248577810203549` */
7
+ blue: number;
8
+ /** @example `1` */
9
+ alpha: number;
10
+ };
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.customColorFromCSS = void 0;
7
+ // @ts-expect-error
8
+ const normalize_color_1 = __importDefault(require("@react-native/normalize-color"));
9
+ function customColorFromCSS(color) {
10
+ let colorInt = (0, normalize_color_1.default)(color);
11
+ colorInt = ((colorInt << 24) | (colorInt >>> 8)) >>> 0;
12
+ const red = ((colorInt >> 16) & 255) / 255;
13
+ const green = ((colorInt >> 8) & 255) / 255;
14
+ const blue = (colorInt & 255) / 255;
15
+ const alpha = ((colorInt >> 24) & 255) / 255;
16
+ return {
17
+ red,
18
+ green,
19
+ blue,
20
+ alpha,
21
+ };
22
+ }
23
+ exports.customColorFromCSS = customColorFromCSS;
@@ -0,0 +1,11 @@
1
+ import { ConfigPlugin } from "@expo/config-plugins";
2
+ export declare const withIosColorset: ConfigPlugin<{
3
+ cwd: string;
4
+ name: string;
5
+ color: string;
6
+ darkColor?: string;
7
+ }>;
8
+ export declare function setColorAsync({ color, darkColor }: {
9
+ color: string;
10
+ darkColor?: string;
11
+ }, colorsetFilePath: string): Promise<void>;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.setColorAsync = exports.withIosColorset = void 0;
7
+ const config_plugins_1 = require("@expo/config-plugins");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const customColorFromCSS_1 = require("./customColorFromCSS");
11
+ const withIosColorset = (config, { cwd, color, darkColor, name }) => {
12
+ return (0, config_plugins_1.withDangerousMod)(config, [
13
+ "ios",
14
+ async (config) => {
15
+ await setColorAsync({ color, darkColor }, node_path_1.default.join(config.modRequest.projectRoot, cwd, `Assets.xcassets/${name}.colorset`));
16
+ return config;
17
+ },
18
+ ]);
19
+ };
20
+ exports.withIosColorset = withIosColorset;
21
+ const DARK_APPEARANCE = {
22
+ appearance: "luminosity",
23
+ value: "dark",
24
+ };
25
+ function createColor(color) {
26
+ return {
27
+ "color-space": "srgb",
28
+ components: (0, customColorFromCSS_1.customColorFromCSS)(color),
29
+ };
30
+ }
31
+ async function setColorAsync({ color, darkColor }, colorsetFilePath) {
32
+ // Ensure the Images.xcassets/AppIcon.appiconset path exists
33
+ await node_fs_1.default.promises.mkdir(colorsetFilePath, { recursive: true });
34
+ // Store the image JSON data for assigning via the Contents.json
35
+ const colorsJson = [];
36
+ if (color) {
37
+ colorsJson.push({
38
+ color: createColor(color),
39
+ idiom: "universal",
40
+ });
41
+ }
42
+ if (darkColor) {
43
+ colorsJson.push({
44
+ appearances: [DARK_APPEARANCE],
45
+ color: createColor(darkColor),
46
+ idiom: "universal",
47
+ });
48
+ }
49
+ // Finally, write the Config.json
50
+ await writeContentsJsonAsync(colorsetFilePath, {
51
+ colors: colorsJson,
52
+ });
53
+ }
54
+ exports.setColorAsync = setColorAsync;
55
+ async function writeContentsJsonAsync(directory, { colors }) {
56
+ await node_fs_1.default.promises.mkdir(directory, { recursive: true });
57
+ await node_fs_1.default.promises.writeFile(node_path_1.default.join(directory, "Contents.json"), JSON.stringify({
58
+ colors,
59
+ info: {
60
+ version: 1,
61
+ // common practice is for the tool that generated the icons to be the "author"
62
+ author: "expo",
63
+ },
64
+ }, null, 2));
65
+ }
@@ -0,0 +1,103 @@
1
+ import { ExtensionType } from "./target";
2
+ export type DynamicColor = {
3
+ light: string;
4
+ dark?: string;
5
+ };
6
+ export type Entitlements = Partial<{
7
+ "com.apple.developer.healthkit": boolean;
8
+ "com.apple.developer.healthkit.access": string[];
9
+ /** prefixed with `merchant.` */
10
+ "com.apple.developer.in-app-payments": string[];
11
+ /** prefixed with `iCloud.` */
12
+ "com.apple.developer.icloud-container-identifiers": string[];
13
+ "com.apple.developer.ClassKit-environment": "production" | "development";
14
+ "com.apple.developer.default-data-protection": "NSFileProtectionCompleteUnlessOpen" | "NSFileProtectionCompleteUntilFirstUserAuthentication" | "NSFileProtectionNone" | "NSFileProtectionComplete";
15
+ "com.apple.developer.networking.networkextension": ("dns-proxy" | "app-proxy-provider" | "content-filter-provider" | "packet-tunnel-provider" | "dns-proxy-systemextension" | "app-proxy-provider-systemextension" | "content-filter-provider-systemextension" | "packet-tunnel-provider-systemextension" | "dns-settings" | "app-push-provider")[];
16
+ "com.apple.developer.networking.vpn.api": "allow-vpn"[];
17
+ "com.apple.developer.networking.HotspotConfiguration": boolean;
18
+ "com.apple.developer.kernel.extended-virtual-addressing": boolean;
19
+ "com.apple.developer.homekit": boolean;
20
+ "com.apple.developer.networking.multipath": boolean;
21
+ "com.apple.external-accessory.wireless-configuration": boolean;
22
+ "inter-app-audio": boolean;
23
+ "com.apple.developer.pass-type-identifiers": "$(TeamIdentifierPrefix)*"[];
24
+ "com.apple.developer.user-fonts": ("app-usage" | "system-installation")[];
25
+ "com.apple.developer.devicecheck.appattest-environment": "development" | "production";
26
+ "com.apple.developer.nfc.readersession.formats": ("NDEF" | "TAG")[];
27
+ "com.apple.developer.applesignin": "Default"[];
28
+ "com.apple.developer.siri": boolean;
29
+ "com.apple.developer.networking.wifi-info": boolean;
30
+ "com.apple.developer.usernotifications.communication": boolean;
31
+ "com.apple.developer.usernotifications.time-sensitive": boolean;
32
+ "com.apple.developer.group-session": boolean;
33
+ "com.apple.developer.family-controls": boolean;
34
+ "com.apple.developer.authentication-services.autofill-credential-provider": boolean;
35
+ "com.apple.developer.game-center": boolean;
36
+ /** prefixed with `group.` */
37
+ "com.apple.security.application-groups": string[];
38
+ "com.apple.developer.fileprovider.testing-mode": boolean;
39
+ "com.apple.developer.healthkit.recalibrate-estimates": boolean;
40
+ "com.apple.developer.maps": boolean;
41
+ "com.apple.developer.user-management": boolean;
42
+ "com.apple.developer.networking.custom-protocol": boolean;
43
+ "com.apple.developer.system-extension.install": boolean;
44
+ "com.apple.developer.push-to-talk": boolean;
45
+ "com.apple.developer.driverkit.transport.usb": boolean;
46
+ "com.apple.developer.kernel.increased-memory-limit": boolean;
47
+ "com.apple.developer.driverkit.communicates-with-drivers": boolean;
48
+ "com.apple.developer.media-device-discovery-extension": boolean;
49
+ "com.apple.developer.driverkit.allow-third-party-userclients": boolean;
50
+ "com.apple.developer.weatherkit": boolean;
51
+ "com.apple.developer.on-demand-install-capable": boolean;
52
+ "com.apple.developer.driverkit.family.scsicontroller": boolean;
53
+ "com.apple.developer.driverkit.family.serial": boolean;
54
+ "com.apple.developer.driverkit.family.networking": boolean;
55
+ "com.apple.developer.driverkit.family.hid.eventservice": boolean;
56
+ "com.apple.developer.driverkit.family.hid.device": boolean;
57
+ "com.apple.developer.driverkit": boolean;
58
+ "com.apple.developer.driverkit.transport.hid": boolean;
59
+ "com.apple.developer.driverkit.family.audio": boolean;
60
+ "com.apple.developer.shared-with-you": boolean;
61
+ }>;
62
+ export type Config = {
63
+ /**
64
+ * The type of extension to generate.
65
+ * @example "widget"
66
+ */
67
+ type: ExtensionType;
68
+ /** Name of the target. Will default to a sanitized version of the directory name. */
69
+ name?: string;
70
+ /**
71
+ * A local file path or URL to an image asset.
72
+ * @example "./assets/icon.png"
73
+ * @example "https://example.com/icon.png"
74
+ */
75
+ icon?: string;
76
+ /**
77
+ * A list of additional frameworks to add to the target.
78
+ * @example ["UserNotifications", "Intents"]
79
+ */
80
+ frameworks?: string[];
81
+ /** Deployment iOS version for the target. Defaults to `16.4` */
82
+ deploymentTarget?: string;
83
+ /** Apple team ID to use for signing the target. Defaults to whatever is used in the main App target. */
84
+ appleTeamId?: string;
85
+ /** Optional entitlements to add to the target. */
86
+ entitlements?: Entitlements;
87
+ /**
88
+ * Additional colors to generate in the `Assets.xcassets`. These will be available as `UIColor`s in the native source.
89
+ * @example In Expo config: `colors: { gradient1: { color: "#FF0000", darkColor: "#0000FF" }`
90
+ * @example In Swift: `Color("gradient1")` -> `#FF0000` in light-mode
91
+ */
92
+ colors?: Record<string, string | DynamicColor>;
93
+ /**
94
+ * Additional images to generate in the `Assets.xcassets`. These will be available as `UIImage`s in the native source. The sources can be URLs or local file paths.
95
+ * @example In Expo config: `images: { evan: "https://github.com/evanbacon.png" }`
96
+ * @example In Swift: `Image("evan")` -> `[picture of guy]`
97
+ */
98
+ images?: Record<string, string | {
99
+ "1x"?: string;
100
+ "2x"?: string;
101
+ "3x"?: string;
102
+ }>;
103
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,26 @@
1
+ import { ConfigPlugin } from "@expo/config-plugins";
2
+ import { ContentsJsonImageIdiom, ContentsJsonImage } from "@expo/prebuild-config/build/plugins/icons/AssetContents";
3
+ export declare const withImageAsset: ConfigPlugin<{
4
+ cwd: string;
5
+ name: string;
6
+ image: string | {
7
+ "1x"?: string;
8
+ "2x"?: string;
9
+ "3x"?: string;
10
+ };
11
+ }>;
12
+ export declare const ICON_CONTENTS: {
13
+ idiom: ContentsJsonImageIdiom;
14
+ sizes: {
15
+ size: number;
16
+ scales: (1 | 2 | 3)[];
17
+ }[];
18
+ }[];
19
+ export declare function setIconsAsync(icon: string, projectRoot: string, iosNamedProjectRoot: string, cacheComponent: string): Promise<void>;
20
+ export declare function generateResizedImageAsync(icon: {
21
+ "1x"?: string;
22
+ "2x"?: string;
23
+ "3x"?: string;
24
+ } | string, name: string, projectRoot: string, iosNamedProjectRoot: string, cacheComponent: string): Promise<ContentsJsonImage[]>;
25
+ export declare function generateIconsInternalAsync(icon: string, projectRoot: string, iosNamedProjectRoot: string, cacheComponent: string): Promise<ContentsJsonImage[]>;
26
+ export declare function generateWatchIconsInternalAsync(icon: string, projectRoot: string, iosNamedProjectRoot: string, cacheComponent: string): Promise<ContentsJsonImage[]>;