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).
130 lines
5.9 KiB
TypeScript
130 lines
5.9 KiB
TypeScript
import { type ConfigPlugin, withPodfile } from "expo/config-plugins";
|
||
|
||
const PATCH_START = "## >>> runtime-framework headers";
|
||
const PATCH_END = "## <<< runtime-framework headers";
|
||
|
||
const EXTRA_HDRS = [
|
||
`\${PODS_CONFIGURATION_BUILD_DIR}/React-RuntimeApple/React_RuntimeApple.framework/Headers`,
|
||
`\${PODS_CONFIGURATION_BUILD_DIR}/React-RuntimeCore/React_RuntimeCore.framework/Headers`,
|
||
`\${PODS_CONFIGURATION_BUILD_DIR}/React-jserrorhandler/React_jserrorhandler.framework/Headers`,
|
||
`\${PODS_CONFIGURATION_BUILD_DIR}/React-jsinspector/jsinspector_modern.framework/Headers`,
|
||
`\${PODS_CONFIGURATION_BUILD_DIR}/React-runtimescheduler/React_runtimescheduler.framework/Headers`,
|
||
`\${PODS_CONFIGURATION_BUILD_DIR}/React-performancetimeline/React_performancetimeline.framework/Headers`,
|
||
`\${PODS_CONFIGURATION_BUILD_DIR}/React-rendererconsistency/React_rendererconsistency.framework/Headers`,
|
||
];
|
||
|
||
function buildPatch(): string {
|
||
return [
|
||
PATCH_START,
|
||
" extra_hdrs = [",
|
||
...EXTRA_HDRS.map((h) => ` "${h}",`),
|
||
" ]",
|
||
"",
|
||
" installer.pods_project.targets.each do |t|",
|
||
" t.build_configurations.each do |cfg|",
|
||
" cfg.build_settings['HEADER_SEARCH_PATHS'] ||= '$(inherited)'",
|
||
" cfg.build_settings['HEADER_SEARCH_PATHS'] << \" #{extra_hdrs.join(' ')}\"",
|
||
" cfg.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'",
|
||
" # iOS 26 / Xcode 26: SwiftUI was split into SwiftUI + SwiftUICore. The SwiftUI",
|
||
" # pods (ExpoUI, glass-effect, glass-poster, …) emit a `-framework SwiftUICore`",
|
||
" # autolink directive that, under use_frameworks :static, flows into the app",
|
||
" # executable's link. The app isn't an allowed client of the private",
|
||
" # SwiftUICore.tbd → `cannot link directly with 'SwiftUICore'`. Dropping that one",
|
||
" # autolink at the Swift frontend lets the symbols resolve via SwiftUI's",
|
||
" # re-export instead. Phone-only — tvOS links fine and must stay untouched.",
|
||
" if ENV['EXPO_TV'] != '1'",
|
||
" cfg.build_settings['OTHER_SWIFT_FLAGS'] ||= '$(inherited)'",
|
||
" cfg.build_settings['OTHER_SWIFT_FLAGS'] << ' -Xfrontend -disable-autolink-framework -Xfrontend SwiftUICore'",
|
||
" end",
|
||
" end",
|
||
" end",
|
||
"",
|
||
" # iOS 26 / Xcode 26: the APP target itself compiles ExpoModulesProvider.swift,",
|
||
" # which imports SwiftUI-based modules (ExpoUI, ExpoGlassEffect, GlassPoster, ExpoBlur, …).",
|
||
" # That emits a `-framework SwiftUICore` autolink into the app executable's OWN object",
|
||
" # files, so the pods-only flag above is not enough — the app's link still fails with",
|
||
" # `cannot link directly with 'SwiftUICore'`. Drop the autolink on the user app target",
|
||
" # too. Phone-only — tvOS has no SwiftUICore split and must stay untouched.",
|
||
" if ENV['EXPO_TV'] != '1'",
|
||
" installer.aggregate_targets.each do |agg|",
|
||
" next unless agg.user_project",
|
||
" agg.user_project.native_targets.each do |target|",
|
||
" target.build_configurations.each do |cfg|",
|
||
" existing = cfg.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)'",
|
||
" existing = existing.join(' ') if existing.is_a?(Array)",
|
||
" unless existing.include?('-disable-autolink-framework -Xfrontend SwiftUICore')",
|
||
" cfg.build_settings['OTHER_SWIFT_FLAGS'] = existing + ' -Xfrontend -disable-autolink-framework -Xfrontend SwiftUICore'",
|
||
" end",
|
||
" end",
|
||
" end",
|
||
" agg.user_project.save",
|
||
" end",
|
||
" end",
|
||
"",
|
||
" # Safely patch RCTThirdPartyComponentsProvider.mm to avoid startup crash on unlinked Fabric components",
|
||
' filepath = "#{installer.sandbox.root}/../build/generated/ios/ReactCodegen/RCTThirdPartyComponentsProvider.mm"',
|
||
" if File.exist?(filepath)",
|
||
" content = File.read(filepath)",
|
||
" if content =~ /thirdPartyComponents = @\\{([\\s\\S]*?)\\};/",
|
||
" entries = $1",
|
||
' new_code = "NSMutableDictionary *dict = [NSMutableDictionary dictionary];\\n"',
|
||
' new_code += " Class cls;\\n"',
|
||
" entries.each_line do |line|",
|
||
" line = line.strip",
|
||
" next if line.empty?",
|
||
' if line =~ /@\\"(.*?)\\":\\s*NSClassFromString\\(@\\"(.*?)\\"\\),?(.*)/',
|
||
" key = $1",
|
||
" val = $2",
|
||
" comment = $3",
|
||
' new_code += " cls = NSClassFromString(@\\"#{val}\\"); if (cls) dict[@\\"#{key}\\"] = cls;#{comment}\\n"',
|
||
" else",
|
||
' new_code += " // #{line}\\n"',
|
||
" end",
|
||
" end",
|
||
' new_code += " thirdPartyComponents = dict;"',
|
||
" content = content.sub(/thirdPartyComponents = @\\{[\\s\\S]*?\\};/, new_code)",
|
||
" File.write(filepath, content)",
|
||
' puts "✅ Patched RCTThirdPartyComponentsProvider.mm for safety"',
|
||
" end",
|
||
" end",
|
||
PATCH_END,
|
||
].join("\n");
|
||
}
|
||
|
||
const withRuntimeFrameworkHeaders: ConfigPlugin = (config) => {
|
||
return withPodfile(config, (config) => {
|
||
let podfile = config.modResults.contents;
|
||
|
||
// 1️⃣ ensure there's a post_install block
|
||
if (!/^\s*post_install\s+do\s+\|installer\|/m.test(podfile)) {
|
||
podfile += `
|
||
|
||
post_install do |installer|
|
||
end
|
||
`;
|
||
}
|
||
|
||
const patch = buildPatch();
|
||
|
||
if (podfile.includes(PATCH_START)) {
|
||
// 🔄 update existing patch
|
||
podfile = podfile.replace(
|
||
new RegExp(`${PATCH_START}[\\s\\S]*?${PATCH_END}`),
|
||
patch,
|
||
);
|
||
} else {
|
||
// ➕ insert right after the post_install opening line
|
||
podfile = podfile.replace(
|
||
/^\s*post_install\s+do\s+\|installer\|.*$/m,
|
||
(match) => `${match}\n\n${patch}`,
|
||
);
|
||
}
|
||
|
||
console.log("✅ with-runtime-framework-headers: Podfile updated");
|
||
config.modResults.contents = podfile;
|
||
return config;
|
||
});
|
||
};
|
||
|
||
export default withRuntimeFrameworkHeaders;
|