@bacons/apple-targets 0.2.0 → 3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 evanbacon
3
+ Copyright (c) 2025 Evan Bacon
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -9,7 +9,7 @@ An experimental Expo Config Plugin that generates native Apple Targets like Widg
9
9
 
10
10
  ## 🚀 How to use
11
11
 
12
- > This plugin requires at least CocoaPods 1.16.2 (ruby 3.2.0), Xcode 16 (macOS 15 Sequoia), and Expo SDK +52.
12
+ > This plugin requires at least CocoaPods 1.16.2 (ruby 3.2.0), Xcode 16 (macOS 15 Sequoia), and Expo SDK +53.
13
13
 
14
14
  1. Run `npx create-target` in your Expo project to generate an Apple target.
15
15
  2. Select a target to generate, I recommend starting with a `widget` (e.g. `npx create-target widget`). This will generate the required widget files in the root `/targets` directory, install `@bacons/apple-targets`, and add the Expo Config Plugin to your project.
@@ -42,7 +42,7 @@ Some targets have special entitlements behavior:
42
42
  Any changes you make outside of the `expo:targets` directory in Xcode are subject to being overwritten by the next `npx expo prebuild --clean`. Check to see if the settings you want to toggle are available in the Info.plist or the `expo-target.config.js` file.
43
43
  If you modify the `expo-target.config.js` or your root `app.json`, you will need to re-run `npx expo prebuild --clean` to sync the changes.
44
44
 
45
- You can use the custom Prebuild template `--template node_modules/@bacons/apple-targets/prebuild-blank.tgz` to create a build without React Native, this can make development a bit faster since there's less to compile.
45
+ You can use the custom Prebuild template `--template ./node_modules/@bacons/apple-targets/prebuild-blank.tgz` to create a build without React Native, this can make development a bit faster since there's less to compile. This is an advanced technique for development **NOT PRODUCTION** and is not intended to be used with third-party Config Plugins.
46
46
 
47
47
  ## Target config
48
48
 
@@ -220,6 +220,8 @@ module.exports = {
220
220
 
221
221
  ### `action`
222
222
 
223
+ ![IMG_C2C825ACC8C7-1](https://github.com/user-attachments/assets/8378e022-2061-4da8-9c46-efe3064dd40c)
224
+
223
225
  These show up in the share sheet. The icon should be transparent as it will be masked by the system.
224
226
 
225
227
  ```js
@@ -318,7 +320,7 @@ If you experience issues building widgets, it might be because React Native is s
318
320
  Some workarounds:
319
321
 
320
322
  - Clear the SwiftUI previews cache: `xcrun simctl --set previews delete all`
321
- - Prebuild without React Native: `npx expo prebuild --template node_modules/@bacons/apple-targets/prebuild-blank.tgz --clean`
323
+ - Prebuild without React Native: `npx expo prebuild --template ./node_modules/@bacons/apple-targets/prebuild-blank.tgz --clean`
322
324
  - If the widget doesn't show on the home screen when building the app, use iOS 18. You can long press the app icon and select the widget display options to transform the app icon into the widget.
323
325
 
324
326
  ## Sharing data between targets
@@ -410,8 +412,78 @@ For more advanced uses, I recommend the following resources:
410
412
 
411
413
  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.
412
414
 
415
+ ## Control widgets
416
+
417
+ ![Simulator Screenshot - iPhone 16 - 2025-01-26 at 15 57 44](https://github.com/user-attachments/assets/c989a3bb-112d-4026-a718-49de4cdb2f3e)
418
+
419
+ [Control widgets](https://developer.apple.com/documentation/swiftui/controlwidget) are a type of widget that appears in the control center, Siri suggestions, the lock screen, and Shortcuts.
420
+
421
+ Generally, you'll want to add control widgets to a `widget` target, but they can be added to any target really.
422
+
423
+ You can add multiple intents, they should be in the `[target]/_shared/*.swift` folder so they can be added to the main target as well as the widget target, this is required to make them work correctly.
424
+
425
+ The following is an example of a control widget that launches a universal link for my app.
426
+
427
+ ```swift
428
+ // targets/widget/_shared/intents.swift
429
+
430
+ import AppIntents
431
+ import SwiftUI
432
+ import WidgetKit
433
+
434
+ // TODO: These must be added to the WidgetBundle manually. They need to be linked outside of the _shared folder.
435
+ // @main
436
+ // struct exportWidgets: WidgetBundle {
437
+ // var body: some Widget {
438
+ // widgetControl0()
439
+ // widgetControl1()
440
+ // }
441
+ // }
442
+
443
+ @available(iOS 18.0, *)
444
+ struct widgetControl0: ControlWidget {
445
+ // Unique ID for the control.
446
+ static let kind: String = "com.bacon.clipdemo.0"
447
+ var body: some ControlWidgetConfiguration {
448
+ StaticControlConfiguration(kind: Self.kind) {
449
+ ControlWidgetButton(action: OpenAppIntent0()) {
450
+ // You can also use a custom image but it must be an SF Symbol.
451
+ Label("App Settings", systemImage: "star")
452
+ }
453
+ }
454
+ // This is the configuration for the widget.
455
+ .displayName("Launch Settings")
456
+ .description("A control that launches the app settings.")
457
+ }
458
+ }
459
+
460
+ // This must be in both targets when `openAppWhenRun = true`. We can do that by adding it to the _shared folder.
461
+ // https://developer.apple.com/forums/thread/763851
462
+ @available(iOS 18.0, *)
463
+ struct OpenAppIntent0: ControlConfigurationIntent {
464
+ static let title: LocalizedStringResource = "Launch Settings"
465
+ static let description = IntentDescription(stringLiteral: "A control that launches the app settings.")
466
+ static let isDiscoverable = true
467
+ static let openAppWhenRun: Bool = true
468
+
469
+ @MainActor
470
+ func perform() async throws -> some IntentResult & OpensIntent {
471
+ // Here's the URL we want to launch. It can be any URL but it should be a universal link for your app.
472
+ return .result(opensIntent: OpenURLIntent(URL(string: "https://pillarvalley.expo.app/settings")!))
473
+ }
474
+ }
475
+ ```
476
+
477
+ You should copy the intents into your main `WidgetBundle` struct.
478
+
479
+ Custom images can be used but they must be SF Symbols, you can use a tool like [Create Custom Symbols](https://github.com/jaywcjlove/create-custom-symbols) to do this. Then simply add to the Assets.xcassets folder and reference it in the `Label`.
480
+
481
+ You can do a lot of things with Control Widgets like launching a custom UI instead of opening the app. This plugin should allow for most of these things to work.
482
+
413
483
  ## App Clips
414
484
 
485
+ ![IMG_6BC9D9534F1D-1](https://github.com/user-attachments/assets/f9847f6f-4f0a-44f9-932c-3f8e9703c133)
486
+
415
487
  App Clips leverage the true power of Expo Router, enabling you to link a website and native app to just instantly open the native app on iOS. They're pretty hard to get working though.
416
488
 
417
489
  Here are a few notes from my experience building https://pillarvalley.expo.app (open on iOS to test).
@@ -20,7 +20,7 @@ const withTargetsDir = (config, _props) => {
20
20
  hasWarned = true;
21
21
  console.warn((0, chalk_1.default) `{yellow [bacons/apple-targets]} Expo config is missing required {cyan ios.appleTeamId} property. Find this in Xcode and add to the Expo Config to correct. iOS builds may fail until this is corrected.`);
22
22
  }
23
- const targets = (0, glob_1.sync)(`${root}/${match}/expo-target.config.@(json|js)`, {
23
+ const targets = (0, glob_1.globSync)(`${root}/${match}/expo-target.config.@(json|js)`, {
24
24
  // const targets = globSync(`./targets/action/expo-target.config.@(json|js)`, {
25
25
  cwd: projectRoot,
26
26
  absolute: true,
@@ -27,7 +27,7 @@ exports.generateWatchIconsInternalAsync = exports.generateIconsInternalAsync = e
27
27
  const config_plugins_1 = require("@expo/config-plugins");
28
28
  const image_utils_1 = require("@expo/image-utils");
29
29
  const AssetContents_1 = require("@expo/prebuild-config/build/plugins/icons/AssetContents");
30
- const fs = __importStar(require("fs-extra"));
30
+ const fs = __importStar(require("fs"));
31
31
  const path_1 = __importStar(require("path"));
32
32
  const withImageAsset = (config, { cwd, name, image }) => {
33
33
  return (0, config_plugins_1.withDangerousMod)(config, [
@@ -37,7 +37,9 @@ const withImageAsset = (config, { cwd, name, image }) => {
37
37
  const iosNamedProjectRoot = (0, path_1.join)(projectRoot, cwd);
38
38
  const imgPath = `Assets.xcassets/${name}.imageset`;
39
39
  // Ensure the Images.xcassets/AppIcon.appiconset path exists
40
- await fs.ensureDir((0, path_1.join)(iosNamedProjectRoot, imgPath));
40
+ await fs.promises.mkdir((0, path_1.join)(iosNamedProjectRoot, imgPath), {
41
+ recursive: true,
42
+ });
41
43
  const userDefinedIcon = typeof image === "string"
42
44
  ? { "1x": image, "2x": undefined, "3x": undefined }
43
45
  : image;
@@ -120,7 +122,9 @@ exports.ICON_CONTENTS = [
120
122
  ];
121
123
  async function setIconsAsync(icon, projectRoot, iosNamedProjectRoot, cacheComponent) {
122
124
  // Ensure the Images.xcassets/AppIcon.appiconset path exists
123
- await fs.ensureDir((0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH));
125
+ await fs.promises.mkdir((0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH), {
126
+ recursive: true,
127
+ });
124
128
  // Finally, write the Config.json
125
129
  await (0, AssetContents_1.writeContentsJsonAsync)((0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH), {
126
130
  images: await generateIconsInternalAsync(icon, projectRoot, iosNamedProjectRoot, cacheComponent),
@@ -154,7 +158,7 @@ async function generateResizedImageAsync(icon, name, projectRoot, iosNamedProjec
154
158
  });
155
159
  // Write image buffer to the file system.
156
160
  const assetPath = (0, path_1.join)(iosNamedProjectRoot, `Assets.xcassets/${name}.imageset`, filename);
157
- await fs.writeFile(assetPath, source);
161
+ await fs.promises.writeFile(assetPath, source);
158
162
  if (filename) {
159
163
  imgEntry.filename = filename;
160
164
  }
@@ -196,7 +200,7 @@ async function generateIconsInternalAsync(icon, projectRoot, iosNamedProjectRoot
196
200
  });
197
201
  // Write image buffer to the file system.
198
202
  const assetPath = (0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH, filename);
199
- await fs.writeFile(assetPath, source);
203
+ await fs.promises.writeFile(assetPath, source);
200
204
  // Save a reference to the generated image so we don't create a duplicate.
201
205
  generatedIcons[filename] = true;
202
206
  }
@@ -234,7 +238,7 @@ async function generateWatchIconsInternalAsync(icon, projectRoot, iosNamedProjec
234
238
  });
235
239
  // Write image buffer to the file system.
236
240
  const assetPath = (0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH, filename);
237
- await fs.writeFile(assetPath, source);
241
+ await fs.promises.writeFile(assetPath, source);
238
242
  imagesJson.push({
239
243
  filename: getAppleIconName(size, 1),
240
244
  idiom: "universal",
@@ -27,7 +27,7 @@ exports.generateWatchIconsInternalAsync = exports.generateIconsInternalAsync = e
27
27
  const config_plugins_1 = require("@expo/config-plugins");
28
28
  const image_utils_1 = require("@expo/image-utils");
29
29
  const AssetContents_1 = require("@expo/prebuild-config/build/plugins/icons/AssetContents");
30
- const fs = __importStar(require("fs-extra"));
30
+ const fs = __importStar(require("fs"));
31
31
  const path_1 = require("path");
32
32
  // TODO: support dark, tinted, and universal icons for widgets.
33
33
  const withIosIcon = (config, { cwd, type, iconFilePath, isTransparent = false }) => {
@@ -38,7 +38,9 @@ const withIosIcon = (config, { cwd, type, iconFilePath, isTransparent = false })
38
38
  const namedProjectRoot = (0, path_1.join)(projectRoot, cwd);
39
39
  if (type === "watch") {
40
40
  // Ensure the Images.xcassets/AppIcon.appiconset path exists
41
- await fs.ensureDir((0, path_1.join)(namedProjectRoot, IMAGESET_PATH));
41
+ await fs.promises.mkdir((0, path_1.join)(namedProjectRoot, IMAGESET_PATH), {
42
+ recursive: true,
43
+ });
42
44
  // Finally, write the Config.json
43
45
  await (0, AssetContents_1.writeContentsJsonAsync)((0, path_1.join)(namedProjectRoot, IMAGESET_PATH), {
44
46
  images: await generateWatchIconsInternalAsync(iconFilePath, projectRoot, namedProjectRoot, cwd, isTransparent),
@@ -119,7 +121,9 @@ exports.ICON_CONTENTS = [
119
121
  ];
120
122
  async function setIconsAsync(icon, projectRoot, iosNamedProjectRoot, cacheComponent, isTransparent) {
121
123
  // Ensure the Images.xcassets/AppIcon.appiconset path exists
122
- await fs.ensureDir((0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH));
124
+ await fs.promises.mkdir((0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH), {
125
+ recursive: true,
126
+ });
123
127
  // Finally, write the Config.json
124
128
  await (0, AssetContents_1.writeContentsJsonAsync)((0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH), {
125
129
  images: await generateIconsInternalAsync(icon, projectRoot, iosNamedProjectRoot, cacheComponent, isTransparent),
@@ -158,7 +162,7 @@ async function generateIconsInternalAsync(icon, projectRoot, iosNamedProjectRoot
158
162
  });
159
163
  // Write image buffer to the file system.
160
164
  const assetPath = (0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH, filename);
161
- await fs.writeFile(assetPath, source);
165
+ await fs.promises.writeFile(assetPath, source);
162
166
  // Save a reference to the generated image so we don't create a duplicate.
163
167
  generatedIcons[filename] = true;
164
168
  }
@@ -196,7 +200,7 @@ async function generateWatchIconsInternalAsync(icon, projectRoot, iosNamedProjec
196
200
  });
197
201
  // Write image buffer to the file system.
198
202
  const assetPath = (0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH, filename);
199
- await fs.writeFile(assetPath, source);
203
+ await fs.promises.writeFile(assetPath, source);
200
204
  imagesJson.push({
201
205
  filename: getAppleIconName(size, 1),
202
206
  idiom: "universal",
@@ -65,7 +65,7 @@ const withWidget = (config, props) => {
65
65
  // TODO: Are there characters that aren't allowed in `CFBundleDisplayName`?
66
66
  const targetDisplayName = (_a = props.name) !== null && _a !== void 0 ? _a : productName;
67
67
  const targetDirAbsolutePath = path_1.default.join((_c = (_b = config._internal) === null || _b === void 0 ? void 0 : _b.projectRoot) !== null && _c !== void 0 ? _c : "", props.directory);
68
- const entitlementsFiles = (0, glob_1.sync)("*.entitlements", {
68
+ const entitlementsFiles = (0, glob_1.globSync)("*.entitlements", {
69
69
  absolute: true,
70
70
  cwd: targetDirAbsolutePath,
71
71
  });
@@ -260,6 +260,7 @@ const withWidget = (config, props) => {
260
260
  frameworks: (0, target_1.getFrameworksForType)(props.type).concat(props.frameworks || []),
261
261
  type: props.type,
262
262
  teamId: props.appleTeamId,
263
+ colors: props.colors,
263
264
  exportJs: (_j = props.exportJs) !== null && _j !== void 0 ? _j :
264
265
  // Assume App Clips are used for React Native.
265
266
  props.type === "clip",
@@ -12,7 +12,7 @@ export type XcodeSettings = {
12
12
  frameworks: string[];
13
13
  type: ExtensionType;
14
14
  hasAccentColor?: boolean;
15
- colors?: Record<string, string>;
15
+ colors?: Record<string, any>;
16
16
  teamId?: string;
17
17
  icon?: string;
18
18
  exportJs?: boolean;
@@ -14,9 +14,8 @@ const TemplateBuildSettings = XCBuildConfiguration_json_1.default;
14
14
  const withXcparse_1 = require("./withXcparse");
15
15
  const assert_1 = __importDefault(require("assert"));
16
16
  const withXcodeChanges = (config, props) => {
17
- return (0, withXcparse_1.withXcodeProjectBeta)(config, (config) => {
18
- // @ts-ignore
19
- applyXcodeChanges(config, config.modResults, props);
17
+ return (0, withXcparse_1.withXcodeProjectBeta)(config, async (config) => {
18
+ await applyXcodeChanges(config, config.modResults, props);
20
19
  return config;
21
20
  });
22
21
  };
@@ -553,7 +552,7 @@ function getDeviceFamilyBuildSettings(deviceFamilies) {
553
552
  TARGETED_DEVICE_FAMILY: families.join(","),
554
553
  };
555
554
  }
556
- function createConfigurationList(project, { name, cwd, bundleId, deploymentTarget, currentProjectVersion, }) {
555
+ function createWidgetConfigurationList(project, { name, cwd, bundleId, deploymentTarget, currentProjectVersion, }) {
557
556
  const debugBuildConfig = xcode_1.XCBuildConfiguration.create(project, {
558
557
  name: "Debug",
559
558
  buildSettings: {
@@ -641,7 +640,7 @@ function createConfigurationList(project, { name, cwd, bundleId, deploymentTarge
641
640
  }
642
641
  function createConfigurationListForType(project, props) {
643
642
  if (props.type === "widget") {
644
- return createConfigurationList(project, props);
643
+ return createWidgetConfigurationList(project, props);
645
644
  }
646
645
  else if (props.type === "action") {
647
646
  return createExtensionConfigurationListFromTemplate(project, "com.apple.services", props);
@@ -732,7 +731,7 @@ async function applyXcodeChanges(config, project, props) {
732
731
  }
733
732
  }
734
733
  function configureTargetWithEntitlements(target) {
735
- const entitlements = (0, glob_1.sync)("*.entitlements", {
734
+ const entitlements = (0, glob_1.globSync)("*.entitlements", {
736
735
  absolute: false,
737
736
  cwd: magicCwd,
738
737
  });
@@ -754,7 +753,7 @@ async function applyXcodeChanges(config, project, props) {
754
753
  });
755
754
  }
756
755
  function configureTargetWithPreview(target) {
757
- const assets = (0, glob_1.sync)("preview/*.xcassets", {
756
+ const assets = (0, glob_1.globSync)("preview/*.xcassets", {
758
757
  absolute: true,
759
758
  cwd: magicCwd,
760
759
  })[0];
@@ -874,7 +873,7 @@ async function applyXcodeChanges(config, project, props) {
874
873
  fs_1.default.statSync(path_1.default.join(assetsDir, file)).isDirectory())
875
874
  .map((file) => path_1.default.join("assets", file));
876
875
  const protectedGroup = ensureProtectedGroup(project, path_1.default.dirname(props.cwd));
877
- const sharedAssets = (0, glob_1.sync)("_shared/*", {
876
+ const sharedAssets = (0, glob_1.globSync)("_shared/*", {
878
877
  absolute: false,
879
878
  cwd: magicCwd,
880
879
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacons/apple-targets",
3
- "version": "0.2.0",
3
+ "version": "3.0.0",
4
4
  "description": "Generate Apple Targets with Expo Prebuild",
5
5
  "main": "build/ExtensionStorage.js",
6
6
  "types": "build/ExtensionStorage.d.ts",
@@ -33,15 +33,13 @@
33
33
  "author": "Evan Bacon",
34
34
  "license": "MIT",
35
35
  "dependencies": {
36
- "@react-native/normalize-colors": "^0.76.1",
37
- "glob": "^10.2.6",
38
36
  "@bacons/xcode": "1.0.0-alpha.24",
39
- "fs-extra": "^11.2.0",
37
+ "@react-native/normalize-colors": "^0.79.2",
38
+ "glob": "^10.4.2",
40
39
  "debug": "^4.3.4"
41
40
  },
42
41
  "devDependencies": {
43
42
  "@types/debug": "^4.1.7",
44
- "@types/fs-extra": "^11.0.4",
45
43
  "@types/glob": "^8.1.0",
46
44
  "@expo/babel-preset-cli": "^0.3.1",
47
45
  "chalk": "^4.0.0",
Binary file