@bacons/apple-targets 0.1.19 → 0.2.1
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 +73 -1
- package/build/config-plugin.js +4 -1
- package/build/icon/withImageAsset.js +10 -6
- package/build/icon/withIosIcon.js +9 -5
- package/build/withWidget.js +61 -39
- package/build/withXcodeChanges.d.ts +3 -1
- package/build/withXcodeChanges.js +10 -11
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -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
|
+

|
|
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
|
|
@@ -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
|
+

|
|
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
|
+

|
|
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).
|
package/build/config-plugin.js
CHANGED
|
@@ -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.
|
|
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,
|
|
@@ -38,6 +38,9 @@ const withTargetsDir = (config, _props) => {
|
|
|
38
38
|
else if (typeof targetConfig !== "object") {
|
|
39
39
|
throw new Error(`Expected target config to be an object or function that returns an object, but got ${typeof targetConfig}`);
|
|
40
40
|
}
|
|
41
|
+
if (!evaluatedTargetConfigObject.type) {
|
|
42
|
+
throw new Error(`Expected target config to have a 'type' property denoting the type of target it is, e.g. 'widget'`);
|
|
43
|
+
}
|
|
41
44
|
config = (0, withWidget_1.default)(config, {
|
|
42
45
|
appleTeamId,
|
|
43
46
|
...evaluatedTargetConfigObject,
|
|
@@ -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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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",
|
package/build/withWidget.js
CHANGED
|
@@ -45,30 +45,32 @@ function createLogQueue() {
|
|
|
45
45
|
}
|
|
46
46
|
// Queue up logs so they only run when prebuild is actually running and not during standard config reads.
|
|
47
47
|
const prebuildLogQueue = createLogQueue();
|
|
48
|
-
function kebabToCamelCase(str) {
|
|
49
|
-
return str.replace(/-([a-z])/g, function (g) {
|
|
50
|
-
return g[1].toUpperCase();
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
48
|
const withWidget = (config, props) => {
|
|
54
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j
|
|
49
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
55
50
|
prebuildLogQueue.add(() => warnOnce((0, chalk_1.default) `\nUsing experimental Config Plugin {bold @bacons/apple-targets} that is subject to breaking changes.`));
|
|
56
51
|
// TODO: Magically based on the top-level folders in the `ios-widgets/` folder
|
|
57
52
|
if (props.icon && !/https?:\/\//.test(props.icon)) {
|
|
58
53
|
props.icon = path_1.default.join(props.directory, props.icon);
|
|
59
54
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
// This value should be used for the target name and other internal uses.
|
|
56
|
+
const targetDirName = path_1.default.basename(path_1.default.dirname(props.configPath));
|
|
57
|
+
// Sanitized for general usage. This name just needs to resemble the input value since it shouldn't be used for user-facing values such as the home screen or app store.
|
|
58
|
+
const productName = sanitizeNameForNonDisplayUse(props.name || targetDirName) ||
|
|
59
|
+
sanitizeNameForNonDisplayUse(targetDirName) ||
|
|
60
|
+
sanitizeNameForNonDisplayUse(props.type);
|
|
61
|
+
// This should never happen.
|
|
62
|
+
if (!productName) {
|
|
63
|
+
throw new Error(`[bacons/apple-targets][${props.type}] Target name does not contain any valid characters: ${targetDirName}`);
|
|
64
|
+
}
|
|
65
|
+
// TODO: Are there characters that aren't allowed in `CFBundleDisplayName`?
|
|
66
|
+
const targetDisplayName = (_a = props.name) !== null && _a !== void 0 ? _a : productName;
|
|
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.globSync)("*.entitlements", {
|
|
67
69
|
absolute: true,
|
|
68
|
-
cwd:
|
|
70
|
+
cwd: targetDirAbsolutePath,
|
|
69
71
|
});
|
|
70
72
|
if (entitlementsFiles.length > 1) {
|
|
71
|
-
throw new Error(`[bacons/apple-targets][${props.type}] Found more than one '*.entitlements' file in ${
|
|
73
|
+
throw new Error(`[bacons/apple-targets][${props.type}] Found more than one '*.entitlements' file in ${targetDirAbsolutePath}`);
|
|
72
74
|
}
|
|
73
75
|
let entitlementsJson = props.entitlements;
|
|
74
76
|
if (entitlementsJson) {
|
|
@@ -87,7 +89,7 @@ const withWidget = (config, props) => {
|
|
|
87
89
|
if (!associatedDomains ||
|
|
88
90
|
!Array.isArray(associatedDomains) ||
|
|
89
91
|
associatedDomains.length === 0) {
|
|
90
|
-
warnOnce((0, chalk_1.default) `{yellow [${
|
|
92
|
+
warnOnce((0, chalk_1.default) `{yellow [${targetDirName}]} Apple App Clip may require the associated domains entitlement but none were found in the Expo config.\nExample:\n${JSON.stringify({
|
|
91
93
|
ios: {
|
|
92
94
|
associatedDomains: [`applinks:placeholder.expo.app`],
|
|
93
95
|
},
|
|
@@ -110,7 +112,7 @@ const withWidget = (config, props) => {
|
|
|
110
112
|
.filter(Boolean);
|
|
111
113
|
const unique = [...new Set(sanitizedUrls)];
|
|
112
114
|
if (unique.length) {
|
|
113
|
-
warnOnce((0, chalk_1.default) `{gray [${
|
|
115
|
+
warnOnce((0, chalk_1.default) `{gray [${targetDirName}]} Apple App Clip expo-target.config.js missing associated domains entitlements in the target config. Using the following defaults:\n${JSON.stringify({
|
|
114
116
|
entitlements: {
|
|
115
117
|
[associatedDomainsKey]: [
|
|
116
118
|
`appclips:${unique[0] || "mywebsite.expo.app"}`,
|
|
@@ -137,13 +139,13 @@ const withWidget = (config, props) => {
|
|
|
137
139
|
// Then set the target app groups to match the main app.
|
|
138
140
|
entitlements[APP_GROUP_KEY] = mainAppGroups;
|
|
139
141
|
prebuildLogQueue.add(() => {
|
|
140
|
-
logOnce((0, chalk_1.default) `[${
|
|
142
|
+
logOnce((0, chalk_1.default) `[${targetDirName}] Syncing app groups with main app. {dim Define entitlements[${JSON.stringify(APP_GROUP_KEY)}] in the {bold expo-target.config} file to override.}`);
|
|
141
143
|
});
|
|
142
144
|
}
|
|
143
145
|
else {
|
|
144
146
|
prebuildLogQueue.add(() => {
|
|
145
147
|
var _a, _b;
|
|
146
|
-
return warnOnce((0, chalk_1.default) `{yellow [${
|
|
148
|
+
return warnOnce((0, chalk_1.default) `{yellow [${targetDirName}]} Apple target may require the App Groups entitlement but none were found in the Expo config.\nExample:\n${JSON.stringify({
|
|
147
149
|
ios: {
|
|
148
150
|
entitlements: {
|
|
149
151
|
[APP_GROUP_KEY]: [
|
|
@@ -168,11 +170,11 @@ const withWidget = (config, props) => {
|
|
|
168
170
|
const GENERATED_ENTITLEMENTS_FILE_NAME = "generated.entitlements";
|
|
169
171
|
const entitlementsFilePath = (_a = entitlementsFiles[0]) !== null && _a !== void 0 ? _a :
|
|
170
172
|
// Use the name `generated` to help indicate that this file should be in sync with the config
|
|
171
|
-
path_1.default.join(
|
|
173
|
+
path_1.default.join(targetDirAbsolutePath, GENERATED_ENTITLEMENTS_FILE_NAME);
|
|
172
174
|
if (entitlementsFiles[0]) {
|
|
173
|
-
const relativeName = path_1.default.relative(
|
|
175
|
+
const relativeName = path_1.default.relative(targetDirAbsolutePath, entitlementsFiles[0]);
|
|
174
176
|
if (relativeName !== GENERATED_ENTITLEMENTS_FILE_NAME) {
|
|
175
|
-
console.log(`[${
|
|
177
|
+
console.log(`[${targetDirName}] Replacing ${path_1.default.relative(targetDirAbsolutePath, entitlementsFiles[0])} with entitlements JSON from config`);
|
|
176
178
|
}
|
|
177
179
|
}
|
|
178
180
|
fs_1.default.writeFileSync(entitlementsFilePath, plist_1.default.build(entitlementsJson));
|
|
@@ -190,7 +192,7 @@ const withWidget = (config, props) => {
|
|
|
190
192
|
"ios",
|
|
191
193
|
async (config) => {
|
|
192
194
|
prebuildLogQueue.flush();
|
|
193
|
-
fs_1.default.mkdirSync(
|
|
195
|
+
fs_1.default.mkdirSync(targetDirAbsolutePath, { recursive: true });
|
|
194
196
|
const files = [
|
|
195
197
|
["Info.plist", (0, target_1.getTargetInfoPlistForType)(props.type)],
|
|
196
198
|
];
|
|
@@ -208,7 +210,7 @@ const withWidget = (config, props) => {
|
|
|
208
210
|
// );
|
|
209
211
|
// }
|
|
210
212
|
files.forEach(([filename, content]) => {
|
|
211
|
-
const filePath = path_1.default.join(
|
|
213
|
+
const filePath = path_1.default.join(targetDirAbsolutePath, filename);
|
|
212
214
|
if (!fs_1.default.existsSync(filePath)) {
|
|
213
215
|
fs_1.default.writeFileSync(filePath, content);
|
|
214
216
|
}
|
|
@@ -216,41 +218,55 @@ const withWidget = (config, props) => {
|
|
|
216
218
|
return config;
|
|
217
219
|
},
|
|
218
220
|
]);
|
|
219
|
-
const targetName = (_c = props.name) !== null && _c !== void 0 ? _c : widget;
|
|
220
221
|
const mainAppBundleId = config.ios.bundleIdentifier;
|
|
221
|
-
const bundleId = ((
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
222
|
+
const bundleId = (() => {
|
|
223
|
+
var _a;
|
|
224
|
+
// Support the bundle identifier being appended to the main app's bundle identifier.
|
|
225
|
+
if ((_a = props.bundleIdentifier) === null || _a === void 0 ? void 0 : _a.startsWith(".")) {
|
|
226
|
+
return mainAppBundleId + props.bundleIdentifier;
|
|
227
|
+
}
|
|
228
|
+
else if (props.bundleIdentifier) {
|
|
229
|
+
return props.bundleIdentifier;
|
|
230
|
+
}
|
|
231
|
+
if (props.type === "clip") {
|
|
232
|
+
// Use a more standardized bundle identifier for App Clips.
|
|
233
|
+
return mainAppBundleId + ".clip";
|
|
234
|
+
}
|
|
235
|
+
let bundleId = mainAppBundleId;
|
|
236
|
+
bundleId += ".";
|
|
237
|
+
// Generate the bundle identifier. This logic needs to remain generally stable since it's used for a permanent value.
|
|
238
|
+
// Key here is simplicity and predictability since it's already appended to the main app's bundle identifier.
|
|
239
|
+
return mainAppBundleId + "." + getSanitizedBundleIdentifier(props.type);
|
|
240
|
+
})();
|
|
241
|
+
const deviceFamilies = ((_d = config.ios) === null || _d === void 0 ? void 0 : _d.isTabletOnly)
|
|
228
242
|
? ["tablet"]
|
|
229
|
-
: ((
|
|
243
|
+
: ((_e = config.ios) === null || _e === void 0 ? void 0 : _e.supportsTablet)
|
|
230
244
|
? ["phone", "tablet"]
|
|
231
245
|
: ["phone"];
|
|
232
246
|
(0, withXcodeChanges_1.withXcodeChanges)(config, {
|
|
247
|
+
productName,
|
|
233
248
|
configPath: props.configPath,
|
|
234
|
-
name:
|
|
249
|
+
name: targetDisplayName,
|
|
235
250
|
cwd: "../" +
|
|
236
251
|
path_1.default.relative(config._internal.projectRoot, path_1.default.resolve(props.directory)),
|
|
237
|
-
deploymentTarget: (
|
|
252
|
+
deploymentTarget: (_f = props.deploymentTarget) !== null && _f !== void 0 ? _f : DEFAULT_DEPLOYMENT_TARGET,
|
|
238
253
|
bundleId,
|
|
239
254
|
icon: props.icon,
|
|
240
255
|
orientation: config.orientation,
|
|
241
|
-
hasAccentColor: !!((
|
|
256
|
+
hasAccentColor: !!((_g = props.colors) === null || _g === void 0 ? void 0 : _g.$accent),
|
|
242
257
|
deviceFamilies,
|
|
243
258
|
// @ts-expect-error: who cares
|
|
244
|
-
currentProjectVersion: ((
|
|
259
|
+
currentProjectVersion: ((_h = config.ios) === null || _h === void 0 ? void 0 : _h.buildNumber) || 1,
|
|
245
260
|
frameworks: (0, target_1.getFrameworksForType)(props.type).concat(props.frameworks || []),
|
|
246
261
|
type: props.type,
|
|
247
262
|
teamId: props.appleTeamId,
|
|
248
|
-
|
|
263
|
+
colors: props.colors,
|
|
264
|
+
exportJs: (_j = props.exportJs) !== null && _j !== void 0 ? _j :
|
|
249
265
|
// Assume App Clips are used for React Native.
|
|
250
266
|
props.type === "clip",
|
|
251
267
|
});
|
|
252
268
|
config = (0, withEasCredentials_1.withEASTargets)(config, {
|
|
253
|
-
targetName,
|
|
269
|
+
targetName: productName,
|
|
254
270
|
bundleIdentifier: bundleId,
|
|
255
271
|
entitlements: entitlementsJson,
|
|
256
272
|
});
|
|
@@ -306,3 +322,9 @@ function getSanitizedBundleIdentifier(value) {
|
|
|
306
322
|
// Can have empty segments (e.g. com.example..app).
|
|
307
323
|
return value.replace(/(^[^a-zA-Z.-]|[^a-zA-Z0-9-.])/g, "-");
|
|
308
324
|
}
|
|
325
|
+
function sanitizeNameForNonDisplayUse(name) {
|
|
326
|
+
return name
|
|
327
|
+
.replace(/[\W_]+/g, "")
|
|
328
|
+
.normalize("NFD")
|
|
329
|
+
.replace(/[\u0300-\u036f]/g, "");
|
|
330
|
+
}
|
|
@@ -2,6 +2,8 @@ import { ConfigPlugin } from "@expo/config-plugins";
|
|
|
2
2
|
import { ExtensionType } from "./target";
|
|
3
3
|
export type XcodeSettings = {
|
|
4
4
|
name: string;
|
|
5
|
+
/** Name used for internal purposes. This has more strict rules and should be generated. */
|
|
6
|
+
productName: string;
|
|
5
7
|
/** Directory relative to the project root, (i.e. outside of the `ios` directory) where the widget code should live. */
|
|
6
8
|
cwd: string;
|
|
7
9
|
bundleId: string;
|
|
@@ -10,7 +12,7 @@ export type XcodeSettings = {
|
|
|
10
12
|
frameworks: string[];
|
|
11
13
|
type: ExtensionType;
|
|
12
14
|
hasAccentColor?: boolean;
|
|
13
|
-
colors?: Record<string,
|
|
15
|
+
colors?: Record<string, any>;
|
|
14
16
|
teamId?: string;
|
|
15
17
|
icon?: string;
|
|
16
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
|
-
|
|
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
|
|
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
|
|
643
|
+
return createWidgetConfigurationList(project, props);
|
|
645
644
|
}
|
|
646
645
|
else if (props.type === "action") {
|
|
647
646
|
return createExtensionConfigurationListFromTemplate(project, "com.apple.services", props);
|
|
@@ -684,7 +683,7 @@ async function applyXcodeChanges(config, project, props) {
|
|
|
684
683
|
});
|
|
685
684
|
}
|
|
686
685
|
const targets = getExtensionTargets();
|
|
687
|
-
const productName = props.
|
|
686
|
+
const productName = props.productName;
|
|
688
687
|
let targetToUpdate = (_a = targets.find((target) => target.props.productName === productName)) !== null && _a !== void 0 ? _a : targets[0];
|
|
689
688
|
if (targetToUpdate) {
|
|
690
689
|
console.log(`Target "${targetToUpdate.props.productName}" already exists, updating instead of creating a new one`);
|
|
@@ -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.
|
|
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.
|
|
756
|
+
const assets = (0, glob_1.globSync)("preview/*.xcassets", {
|
|
758
757
|
absolute: true,
|
|
759
758
|
cwd: magicCwd,
|
|
760
759
|
})[0];
|
|
@@ -833,7 +832,7 @@ async function applyXcodeChanges(config, project, props) {
|
|
|
833
832
|
? "wrapper.extensionkit-extension"
|
|
834
833
|
: "wrapper.app-extension",
|
|
835
834
|
includeInIndex: 0,
|
|
836
|
-
path:
|
|
835
|
+
path: props.name + (isExtension ? ".appex" : ".app"),
|
|
837
836
|
sourceTree: "BUILT_PRODUCTS_DIR",
|
|
838
837
|
}),
|
|
839
838
|
settings: {
|
|
@@ -845,7 +844,7 @@ async function applyXcodeChanges(config, project, props) {
|
|
|
845
844
|
appExtensionBuildFile.props.fileRef);
|
|
846
845
|
targetToUpdate = project.rootObject.createNativeTarget({
|
|
847
846
|
buildConfigurationList: createConfigurationListForType(project, props),
|
|
848
|
-
name:
|
|
847
|
+
name: props.name,
|
|
849
848
|
productName,
|
|
850
849
|
// @ts-expect-error
|
|
851
850
|
productReference: appExtensionBuildFile.props.fileRef /* alphaExtension.appex */,
|
|
@@ -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.
|
|
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.1
|
|
3
|
+
"version": "0.2.1",
|
|
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
|
-
"
|
|
37
|
+
"@react-native/normalize-colors": "^0.76.1",
|
|
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",
|