@duell10111/apple-targets
Version:
Generate Apple Targets with Expo Prebuild
257 lines (256 loc) • 12.5 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const config_plugins_1 = require("@expo/config-plugins");
const plist_1 = __importDefault(require("@expo/plist"));
const fs_1 = __importDefault(require("fs"));
const glob_1 = require("glob");
const path_1 = __importDefault(require("path"));
const chalk_1 = __importDefault(require("chalk"));
const withIosColorset_1 = require("./colorset/withIosColorset");
const withImageAsset_1 = require("./icon/withImageAsset");
const withIosIcon_1 = require("./icon/withIosIcon");
const target_1 = require("./target");
const withEasCredentials_1 = require("./withEasCredentials");
const withXcodeChanges_1 = require("./withXcodeChanges");
const DEFAULT_DEPLOYMENT_TARGET = "18.1";
function memoize(fn) {
const cache = new Map();
return ((...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
});
}
const warnOnce = memoize(console.warn);
const logOnce = memoize(console.log);
function createLogQueue() {
const queue = [];
const flush = () => {
queue.forEach((fn) => fn());
queue.length = 0;
};
return {
flush,
add: (fn) => {
queue.push(fn);
},
};
}
// Queue up logs so they only run when prebuild is actually running and not during standard config reads.
const prebuildLogQueue = createLogQueue();
function kebabToCamelCase(str) {
return str.replace(/-([a-z])/g, function (g) {
return g[1].toUpperCase();
});
}
const withWidget = (config, props) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
prebuildLogQueue.add(() => warnOnce((0, chalk_1.default) `\nUsing experimental Config Plugin {bold @bacons/apple-targets} that is subject to breaking changes.`));
// TODO: Magically based on the top-level folders in the `ios-widgets/` folder
if (props.icon && !/https?:\/\//.test(props.icon)) {
props.icon = path_1.default.join(props.directory, props.icon);
}
const widgetDir = path_1.default
.basename(props.directory)
.replace(/\/+$/, "")
.replace(/^\/+/, "");
const widget = kebabToCamelCase(widgetDir);
const widgetFolderAbsolutePath = path_1.default.join((_b = (_a = config._internal) === null || _a === void 0 ? void 0 : _a.projectRoot) !== null && _b !== void 0 ? _b : "", props.directory);
const entitlementsFiles = (0, glob_1.sync)("*.entitlements", {
absolute: true,
cwd: widgetFolderAbsolutePath,
});
if (entitlementsFiles.length > 1) {
throw new Error(`[bacons/apple-targets][${props.type}] Found more than one '*.entitlements' file in ${widgetFolderAbsolutePath}`);
}
let entitlementsJson = props.entitlements;
if (entitlementsJson) {
// Apply default entitlements that must be present for a target to work.
const applyDefaultEntitlements = (entitlements) => {
var _a, _b;
if (props.type === "clip") {
entitlements["com.apple.developer.parent-application-identifiers"] = [
`$(AppIdentifierPrefix)${config.ios.bundleIdentifier}`,
];
// NOTE: This doesn't seem to be required anymore (Oct 12 2024):
// entitlements["com.apple.developer.on-demand-install-capable"] = true;
}
const APP_GROUP_KEY = "com.apple.security.application-groups";
const hasDefinedAppGroupsManually = APP_GROUP_KEY in entitlements;
if (
// If the user hasn't manually defined the app groups array.
!hasDefinedAppGroupsManually &&
// And the target is part of a predefined list of types that benefit from app groups that match the main app...
target_1.SHOULD_USE_APP_GROUPS_BY_DEFAULT[props.type]) {
const mainAppGroups = (_b = (_a = config.ios) === null || _a === void 0 ? void 0 : _a.entitlements) === null || _b === void 0 ? void 0 : _b[APP_GROUP_KEY];
if (Array.isArray(mainAppGroups) && mainAppGroups.length > 0) {
// Then set the target app groups to match the main app.
entitlements[APP_GROUP_KEY] = mainAppGroups;
prebuildLogQueue.add(() => {
logOnce((0, chalk_1.default) `[${widget}] Syncing app groups with main app. {dim Define entitlements[${JSON.stringify(APP_GROUP_KEY)}] in the {bold expo-target.config} file to override.}`);
});
}
else {
prebuildLogQueue.add(() => {
var _a, _b;
return warnOnce((0, chalk_1.default) `{yellow [${widget}]} Apple target may require the App Groups entitlement but none were found in the Expo config.\nExample:\n${JSON.stringify({
ios: {
entitlements: {
[APP_GROUP_KEY]: [
`group.${(_b = (_a = config.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier) !== null && _b !== void 0 ? _b : `com.example.${config.slug}`}`,
],
},
},
}, null, 2)}`);
});
}
}
return entitlements;
};
entitlementsJson = applyDefaultEntitlements(entitlementsJson);
}
// If the user defined entitlements, then overwrite any existing entitlements file
if (entitlementsJson) {
(0, config_plugins_1.withDangerousMod)(config, [
"ios",
async (config) => {
var _a;
const GENERATED_ENTITLEMENTS_FILE_NAME = "generated.entitlements";
const entitlementsFilePath = (_a = entitlementsFiles[0]) !== null && _a !== void 0 ? _a :
// Use the name `generated` to help indicate that this file should be in sync with the config
path_1.default.join(widgetFolderAbsolutePath, GENERATED_ENTITLEMENTS_FILE_NAME);
if (entitlementsFiles[0]) {
const relativeName = path_1.default.relative(widgetFolderAbsolutePath, entitlementsFiles[0]);
if (relativeName !== GENERATED_ENTITLEMENTS_FILE_NAME) {
console.log(`[${widget}] Replacing ${path_1.default.relative(widgetFolderAbsolutePath, entitlementsFiles[0])} with entitlements JSON from config`);
}
}
fs_1.default.writeFileSync(entitlementsFilePath, plist_1.default.build(entitlementsJson));
return config;
},
]);
}
else {
entitlementsJson = entitlementsFiles[0]
? plist_1.default.parse(fs_1.default.readFileSync(entitlementsFiles[0], "utf8"))
: undefined;
}
// Ensure the entry file exists
(0, config_plugins_1.withDangerousMod)(config, [
"ios",
async (config) => {
prebuildLogQueue.flush();
fs_1.default.mkdirSync(widgetFolderAbsolutePath, { recursive: true });
const files = [
["Info.plist", (0, target_1.getTargetInfoPlistForType)(props.type)],
];
// if (props.type === "widget") {
// files.push(
// [
// "index.swift",
// ENTRY_FILE.replace(
// "// Export widgets here",
// "// Export widgets here\n" + ` ${widget}()`
// ),
// ],
// [widget + ".swift", WIDGET.replace(/alpha/g, widget)],
// [widget + ".intentdefinition", INTENT_DEFINITION]
// );
// }
files.forEach(([filename, content]) => {
const filePath = path_1.default.join(widgetFolderAbsolutePath, filename);
if (!fs_1.default.existsSync(filePath)) {
fs_1.default.writeFileSync(filePath, content);
}
});
return config;
},
]);
const targetName = (_c = props.name) !== null && _c !== void 0 ? _c : widget;
const mainAppBundleId = config.ios.bundleIdentifier;
const bundleId = ((_d = props.bundleIdentifier) === null || _d === void 0 ? void 0 : _d.startsWith("."))
? mainAppBundleId + props.bundleIdentifier
: (_e = props.bundleIdentifier) !== null && _e !== void 0 ? _e : `${mainAppBundleId}.${getSanitizedBundleIdentifier(targetName)}`;
(0, withXcodeChanges_1.withXcodeChanges)(config, {
configPath: props.configPath,
name: targetName,
cwd: "../" +
path_1.default.relative(config._internal.projectRoot, path_1.default.resolve(props.directory)),
deploymentTarget: (_f = props.deploymentTarget) !== null && _f !== void 0 ? _f : DEFAULT_DEPLOYMENT_TARGET,
bundleId,
icon: props.icon,
hasAccentColor: !!((_g = props.colors) === null || _g === void 0 ? void 0 : _g.$accent),
// @ts-expect-error: who cares
currentProjectVersion: ((_h = config.ios) === null || _h === void 0 ? void 0 : _h.buildNumber) || 1,
frameworks: (0, target_1.getFrameworksForType)(props.type).concat(props.frameworks || []),
type: props.type,
teamId: props.appleTeamId,
swiftDependencies: props.swiftDependencies,
exportJs: (_j = props.exportJs) !== null && _j !== void 0 ? _j :
// Assume App Clips are used for React Native.
props.type === "clip",
});
config = (0, withEasCredentials_1.withEASTargets)(config, {
targetName,
bundleIdentifier: bundleId,
entitlements: entitlementsJson,
});
if (props.images) {
Object.entries(props.images).forEach(([name, image]) => {
(0, withImageAsset_1.withImageAsset)(config, {
image,
name,
cwd: props.directory,
});
});
}
withConfigColors(config, props);
if (props.icon) {
(0, withIosIcon_1.withIosIcon)(config, {
type: props.type,
cwd: props.directory,
// TODO: read from the top-level icon.png file in the folder -- ERR this doesn't allow for URLs
iconFilePath: props.icon,
isTransparent: ["action"].includes(props.type),
});
}
return config;
};
const withConfigColors = (config, props) => {
var _a;
props.colors = (_a = props.colors) !== null && _a !== void 0 ? _a : {};
// const colors: NonNullable<Props["colors"]> = props.colors ?? {};
// You use the WidgetBackground and `$accent` to style the widget configuration interface of a configurable widget. Apple could have chosen names to make that more obvious.
// https://useyourloaf.com/blog/widget-background-and-accent-color/
// i.e. when you press and hold on a widget to configure it, the background color of the widget configuration interface changes to the background color we set here.
// if (props.widgetBackgroundColor)
// colors["$widgetBackground"] = props.widgetBackgroundColor;
// if (props.accentColor) colors["AccentColor"] = props.accentColor;
if (props.colors) {
Object.entries(props.colors).forEach(([name, color]) => {
(0, withIosColorset_1.withIosColorset)(config, {
cwd: props.directory,
name,
color: typeof color === "string" ? color : color.light,
darkColor: typeof color === "string" ? undefined : color.dark,
});
});
}
// TODO: Add clean-up maybe? This would possibly restrict the ability to create native colors outside of the Expo target config.
return config;
};
exports.default = withWidget;
function getSanitizedBundleIdentifier(value) {
// According to the behavior observed when using the UI in Xcode.
// Must start with a letter, period, or hyphen (not number).
// Can only contain alphanumeric characters, periods, and hyphens.
// Can have empty segments (e.g. com.example..app).
return value.replace(/(^[^a-zA-Z.-]|[^a-zA-Z0-9-.])/g, "-");
}