mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-11 16:30:24 +01:00
Migrate the dynamic Expo config and all 12 local config plugins from CommonJS .js to typed TypeScript: - app.config.js -> app.config.ts (typed ConfigContext/ExpoConfig, behavior-identical port) - plugins/*.js -> plugins/*.ts with `ConfigPlugin` typings from expo/config-plugins; plugin options are now type-checked (withGitPod) - app.json plugin references updated to the .ts paths - imports unified on expo/config-plugins (some plugins used the @expo/config-plugins alias) Node evaluates the config at prebuild time and cannot parse TypeScript plugin modules on its own (verified empirically: Expo transpiles app.config.ts itself but not its imports), so the documented tsx approach is used: `import "tsx/cjs"` at the top of app.config.ts plus tsx as a devDependency. Validation: resolved prebuild configs (expo config --type prebuild) are byte-identical to the old JS config for both mobile and TV (modulo plugin path extensions and the builtAt timestamp); full `bun run prebuild` and `bun run prebuild:tv` pass and all Android plugin mods are present in the generated project (media3 exclusions, gradle properties, cast activity, network security config, alert colors).
214 lines
6.5 KiB
TypeScript
214 lines
6.5 KiB
TypeScript
import type { ExpoConfig } from "expo/config";
|
|
import {
|
|
type ConfigPlugin,
|
|
withEntitlementsPlist,
|
|
withInfoPlist,
|
|
withXcodeProject,
|
|
} from "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";
|
|
|
|
interface AppExtensionConfig {
|
|
targetName: string;
|
|
bundleIdentifier: string;
|
|
entitlements: {
|
|
"com.apple.security.application-groups": string[];
|
|
"keychain-access-groups": string[];
|
|
};
|
|
}
|
|
|
|
function getBundleIdentifier(config: ExpoConfig): string {
|
|
return config.ios?.bundleIdentifier || "com.fredrikburmester.streamyfin";
|
|
}
|
|
|
|
function getAppGroupIdentifier(config: ExpoConfig): string {
|
|
return `group.${getBundleIdentifier(config)}.tvtopshelf`;
|
|
}
|
|
|
|
function getKeychainAccessGroupIdentifier(config: ExpoConfig): string {
|
|
return `$(AppIdentifierPrefix)${getBundleIdentifier(config)}`;
|
|
}
|
|
|
|
// The xcode project object has no usable typings — keep `any` here.
|
|
function getBuildConfigurations(project: any, configurationListId: string) {
|
|
const configurationList =
|
|
project.hash.project.objects.XCConfigurationList[configurationListId];
|
|
|
|
if (!configurationList?.buildConfigurations) return [];
|
|
|
|
const buildConfigurations = project.pbxXCBuildConfigurationSection();
|
|
return configurationList.buildConfigurations
|
|
.map((config: { value: string }) => buildConfigurations[config.value])
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function ensureAppGroup(value: unknown, appGroupIdentifier: string): string[] {
|
|
const groups = Array.isArray(value) ? value : [];
|
|
return groups.includes(appGroupIdentifier)
|
|
? groups
|
|
: [...groups, appGroupIdentifier];
|
|
}
|
|
|
|
function ensureKeychainAccessGroup(
|
|
value: unknown,
|
|
keychainAccessGroupIdentifier: string,
|
|
): string[] {
|
|
const groups = Array.isArray(value) ? value : [];
|
|
return groups.includes(keychainAccessGroupIdentifier)
|
|
? groups
|
|
: [...groups, keychainAccessGroupIdentifier];
|
|
}
|
|
|
|
function ensureAppExtension(
|
|
appExtensions: unknown,
|
|
targetName: string,
|
|
bundleIdentifier: string,
|
|
appGroupIdentifier: string,
|
|
keychainAccessGroupIdentifier: string,
|
|
): AppExtensionConfig[] {
|
|
const extensionConfig: AppExtensionConfig = {
|
|
targetName,
|
|
bundleIdentifier,
|
|
entitlements: {
|
|
"com.apple.security.application-groups": [appGroupIdentifier],
|
|
"keychain-access-groups": [keychainAccessGroupIdentifier],
|
|
},
|
|
};
|
|
const extensions: AppExtensionConfig[] = 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: ConfigPlugin = (config) => {
|
|
const appGroupIdentifier = getAppGroupIdentifier(config);
|
|
const keychainAccessGroupIdentifier =
|
|
getKeychainAccessGroupIdentifier(config);
|
|
const bundleIdentifier = getBundleIdentifier(config);
|
|
const extensionBundleIdentifier = `${bundleIdentifier}.tvtopshelf`;
|
|
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;
|
|
});
|
|
};
|
|
|
|
export default withTVOSTopShelf;
|