@bacons/apple-targets 0.1.18 → 0.2.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/README.md +7 -2
- package/build/config-plugin.js +3 -0
- package/build/withWidget.js +59 -38
- package/build/withXcodeChanges.d.ts +2 -0
- package/build/withXcodeChanges.js +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -178,8 +178,6 @@ The name of the target must match the name of the target directory.
|
|
|
178
178
|
|
|
179
179
|
Some files are required to be linked to both your target and the main target. To support this, you can add a top-level `_shared` directory. Any file in this directory will be linked to both the main target and the sub-target. You'll need to re-run prebuild every time you add, rename, or remove a file in this directory.
|
|
180
180
|
|
|
181
|
-
This is especially useful for Live Activities and Intents.
|
|
182
|
-
|
|
183
181
|
## `exportJs`
|
|
184
182
|
|
|
185
183
|
The `exportJs` option should be used when the target uses React Native (App Clip, Share extension). It works by linking the main target's `Bundle React Native code and images` build phase to the target. This will ensure that production builds (`Release`) bundle the main JS entry file with Metro, and embed the bundle/assets for offline use.
|
|
@@ -319,6 +317,7 @@ If you experience issues building widgets, it might be because React Native is s
|
|
|
319
317
|
|
|
320
318
|
Some workarounds:
|
|
321
319
|
|
|
320
|
+
- Clear the SwiftUI previews cache: `xcrun simctl --set previews delete all`
|
|
322
321
|
- Prebuild without React Native: `npx expo prebuild --template node_modules/@bacons/apple-targets/prebuild-blank.tgz --clean`
|
|
323
322
|
- 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.
|
|
324
323
|
|
|
@@ -401,6 +400,12 @@ let defaults = UserDefaults(suiteName:
|
|
|
401
400
|
let index = defaults?.string(forKey: "myKey")
|
|
402
401
|
```
|
|
403
402
|
|
|
403
|
+
### More data updates
|
|
404
|
+
|
|
405
|
+
For more advanced uses, I recommend the following resources:
|
|
406
|
+
|
|
407
|
+
- Updating widgets when the app is in the background: [Keeping A Widget Up-to-Date](https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date).
|
|
408
|
+
|
|
404
409
|
## Xcode parsing
|
|
405
410
|
|
|
406
411
|
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/build/config-plugin.js
CHANGED
|
@@ -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,
|
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
|
-
|
|
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);
|
|
66
68
|
const entitlementsFiles = (0, glob_1.sync)("*.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,54 @@ 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
|
-
exportJs: (
|
|
263
|
+
exportJs: (_j = props.exportJs) !== null && _j !== void 0 ? _j :
|
|
249
264
|
// Assume App Clips are used for React Native.
|
|
250
265
|
props.type === "clip",
|
|
251
266
|
});
|
|
252
267
|
config = (0, withEasCredentials_1.withEASTargets)(config, {
|
|
253
|
-
targetName,
|
|
268
|
+
targetName: productName,
|
|
254
269
|
bundleIdentifier: bundleId,
|
|
255
270
|
entitlements: entitlementsJson,
|
|
256
271
|
});
|
|
@@ -306,3 +321,9 @@ function getSanitizedBundleIdentifier(value) {
|
|
|
306
321
|
// Can have empty segments (e.g. com.example..app).
|
|
307
322
|
return value.replace(/(^[^a-zA-Z.-]|[^a-zA-Z0-9-.])/g, "-");
|
|
308
323
|
}
|
|
324
|
+
function sanitizeNameForNonDisplayUse(name) {
|
|
325
|
+
return name
|
|
326
|
+
.replace(/[\W_]+/g, "")
|
|
327
|
+
.normalize("NFD")
|
|
328
|
+
.replace(/[\u0300-\u036f]/g, "");
|
|
329
|
+
}
|
|
@@ -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;
|
|
@@ -589,7 +589,7 @@ function createConfigurationList(project, { name, cwd, bundleId, deploymentTarge
|
|
|
589
589
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS: "DEBUG",
|
|
590
590
|
SWIFT_EMIT_LOC_STRINGS: "YES",
|
|
591
591
|
SWIFT_OPTIMIZATION_LEVEL: "-Onone",
|
|
592
|
-
SWIFT_VERSION: "5",
|
|
592
|
+
SWIFT_VERSION: "5.0",
|
|
593
593
|
TARGETED_DEVICE_FAMILY: "1,2",
|
|
594
594
|
},
|
|
595
595
|
});
|
|
@@ -628,7 +628,7 @@ function createConfigurationList(project, { name, cwd, bundleId, deploymentTarge
|
|
|
628
628
|
SWIFT_EMIT_LOC_STRINGS: "YES",
|
|
629
629
|
SWIFT_COMPILATION_MODE: "wholemodule",
|
|
630
630
|
SWIFT_OPTIMIZATION_LEVEL: "-O",
|
|
631
|
-
SWIFT_VERSION: "5",
|
|
631
|
+
SWIFT_VERSION: "5.0",
|
|
632
632
|
TARGETED_DEVICE_FAMILY: "1,2",
|
|
633
633
|
},
|
|
634
634
|
});
|
|
@@ -684,7 +684,7 @@ async function applyXcodeChanges(config, project, props) {
|
|
|
684
684
|
});
|
|
685
685
|
}
|
|
686
686
|
const targets = getExtensionTargets();
|
|
687
|
-
const productName = props.
|
|
687
|
+
const productName = props.productName;
|
|
688
688
|
let targetToUpdate = (_a = targets.find((target) => target.props.productName === productName)) !== null && _a !== void 0 ? _a : targets[0];
|
|
689
689
|
if (targetToUpdate) {
|
|
690
690
|
console.log(`Target "${targetToUpdate.props.productName}" already exists, updating instead of creating a new one`);
|
|
@@ -833,7 +833,7 @@ async function applyXcodeChanges(config, project, props) {
|
|
|
833
833
|
? "wrapper.extensionkit-extension"
|
|
834
834
|
: "wrapper.app-extension",
|
|
835
835
|
includeInIndex: 0,
|
|
836
|
-
path:
|
|
836
|
+
path: props.name + (isExtension ? ".appex" : ".app"),
|
|
837
837
|
sourceTree: "BUILT_PRODUCTS_DIR",
|
|
838
838
|
}),
|
|
839
839
|
settings: {
|
|
@@ -845,7 +845,7 @@ async function applyXcodeChanges(config, project, props) {
|
|
|
845
845
|
appExtensionBuildFile.props.fileRef);
|
|
846
846
|
targetToUpdate = project.rootObject.createNativeTarget({
|
|
847
847
|
buildConfigurationList: createConfigurationListForType(project, props),
|
|
848
|
-
name:
|
|
848
|
+
name: props.name,
|
|
849
849
|
productName,
|
|
850
850
|
// @ts-expect-error
|
|
851
851
|
productReference: appExtensionBuildFile.props.fileRef /* alphaExtension.appex */,
|