Files
streamyfin/plugins/withTVOSTopShelf.js
2026-05-21 08:47:45 +02:00

197 lines
5.9 KiB
JavaScript

const {
withEntitlementsPlist,
withInfoPlist,
withXcodeProject,
} = require("@expo/config-plugins");
const EXTENSION_TARGET_NAME = "StreamyfinTopShelf";
const TARGET_SOURCE_DIR = "../targets/StreamyfinTopShelf";
const APP_GROUP_INFO_PLIST_KEY = "StreamyfinAppGroupIdentifier";
const KEYCHAIN_ACCESS_GROUP_INFO_PLIST_KEY =
"StreamyfinKeychainAccessGroupIdentifier";
function getBundleIdentifier(config) {
return config.ios?.bundleIdentifier || "com.fredrikburmester.streamyfin";
}
function getAppGroupIdentifier(config) {
return `group.${getBundleIdentifier(config)}`;
}
function getKeychainAccessGroupIdentifier(config) {
return `$(AppIdentifierPrefix)${getBundleIdentifier(config)}`;
}
function getBuildConfigurations(project, configurationListId) {
const configurationList =
project.hash.project.objects.XCConfigurationList[configurationListId];
if (!configurationList?.buildConfigurations) return [];
const buildConfigurations = project.pbxXCBuildConfigurationSection();
return configurationList.buildConfigurations
.map((config) => buildConfigurations[config.value])
.filter(Boolean);
}
function ensureAppGroup(value, appGroupIdentifier) {
const groups = Array.isArray(value) ? value : [];
return groups.includes(appGroupIdentifier)
? groups
: [...groups, appGroupIdentifier];
}
function ensureKeychainAccessGroup(value, keychainAccessGroupIdentifier) {
const groups = Array.isArray(value) ? value : [];
return groups.includes(keychainAccessGroupIdentifier)
? groups
: [...groups, keychainAccessGroupIdentifier];
}
function ensureAppExtension(
appExtensions,
targetName,
bundleIdentifier,
appGroupIdentifier,
keychainAccessGroupIdentifier,
) {
const extensionConfig = {
targetName,
bundleIdentifier,
entitlements: {
"com.apple.security.application-groups": [appGroupIdentifier],
"keychain-access-groups": [keychainAccessGroupIdentifier],
},
};
const extensions = Array.isArray(appExtensions) ? appExtensions : [];
// Keep plugin runs idempotent and preserve unrelated app extension entries.
const existingIndex = extensions.findIndex(
(appExtension) => appExtension?.targetName === targetName,
);
if (existingIndex === -1) {
return [...extensions, extensionConfig];
}
return extensions.map((appExtension, index) =>
index === existingIndex ? extensionConfig : appExtension,
);
}
const withTVOSTopShelf = (config) => {
const appGroupIdentifier = getAppGroupIdentifier(config);
const keychainAccessGroupIdentifier =
getKeychainAccessGroupIdentifier(config);
const bundleIdentifier = getBundleIdentifier(config);
const extensionBundleIdentifier = `${bundleIdentifier}.TopShelf`;
const isTVBuild = process.env.EXPO_TV === "1";
if (isTVBuild) {
config.extra = {
...config.extra,
eas: {
...config.extra?.eas,
build: {
...config.extra?.eas?.build,
experimental: {
...config.extra?.eas?.build?.experimental,
ios: {
...config.extra?.eas?.build?.experimental?.ios,
appExtensions: ensureAppExtension(
config.extra?.eas?.build?.experimental?.ios?.appExtensions,
EXTENSION_TARGET_NAME,
extensionBundleIdentifier,
appGroupIdentifier,
keychainAccessGroupIdentifier,
),
},
},
},
},
};
config = withInfoPlist(config, (config) => {
config.modResults[APP_GROUP_INFO_PLIST_KEY] = appGroupIdentifier;
config.modResults[KEYCHAIN_ACCESS_GROUP_INFO_PLIST_KEY] =
keychainAccessGroupIdentifier;
return config;
});
config = withEntitlementsPlist(config, (config) => {
config.modResults["com.apple.security.application-groups"] =
ensureAppGroup(
config.modResults["com.apple.security.application-groups"],
appGroupIdentifier,
);
config.modResults["keychain-access-groups"] = ensureKeychainAccessGroup(
config.modResults["keychain-access-groups"],
keychainAccessGroupIdentifier,
);
return config;
});
}
if (!isTVBuild) {
return config;
}
return withXcodeProject(config, (config) => {
const project = config.modResults;
if (project.pbxTargetByName(EXTENSION_TARGET_NAME)) {
return config;
}
const target = project.addTarget(
EXTENSION_TARGET_NAME,
"app_extension",
EXTENSION_TARGET_NAME,
extensionBundleIdentifier,
);
project.addBuildPhase(
[`${TARGET_SOURCE_DIR}/TopShelfProvider.swift`],
"PBXSourcesBuildPhase",
"Sources",
target.uuid,
);
project.addBuildPhase(
["TVServices.framework"],
"PBXFrameworksBuildPhase",
"Frameworks",
target.uuid,
);
const buildConfigurations = getBuildConfigurations(
project,
target.pbxNativeTarget.buildConfigurationList,
);
for (const buildConfig of buildConfigurations) {
buildConfig.buildSettings = {
...buildConfig.buildSettings,
CODE_SIGN_ENTITLEMENTS: `${TARGET_SOURCE_DIR}/${EXTENSION_TARGET_NAME}.entitlements`,
APPLICATION_EXTENSION_API_ONLY: "YES",
CURRENT_PROJECT_VERSION: "1",
INFOPLIST_FILE: `${TARGET_SOURCE_DIR}/Info.plist`,
IPHONEOS_DEPLOYMENT_TARGET:
buildConfig.buildSettings.IPHONEOS_DEPLOYMENT_TARGET || "15.6",
MARKETING_VERSION: config.version || "1.0",
PRODUCT_BUNDLE_IDENTIFIER: extensionBundleIdentifier,
PRODUCT_NAME: `"${EXTENSION_TARGET_NAME}"`,
SDKROOT: "appletvos",
SKIP_INSTALL: "YES",
SWIFT_VERSION: "5.9",
APP_GROUP_IDENTIFIER: appGroupIdentifier,
KEYCHAIN_ACCESS_GROUP_IDENTIFIER: `"${keychainAccessGroupIdentifier}"`,
SUPPORTED_PLATFORMS: '"appletvos appletvsimulator"',
TARGETED_DEVICE_FAMILY: "3",
};
}
return config;
});
};
module.exports = withTVOSTopShelf;