From 7054137690685bf49c10fe71ada85f09a58bfb54 Mon Sep 17 00:00:00 2001 From: Gauvino Date: Thu, 11 Jun 2026 12:20:31 +0200 Subject: [PATCH] refactor: migrate app.config and Expo config plugins to TypeScript 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). --- .github/workflows/build-apps.yml | 2 +- app.config.js => app.config.ts | 20 ++++--- app.json | 24 ++++---- bun.lock | 57 +++++++++++++++++++ docs/tv-discovery.md | 2 +- package.json | 1 + ...s.js => with-runtime-framework-headers.ts} | 8 ++- ...ertColors.js => withAndroidAlertColors.ts} | 24 +++++--- ...roidManifest.js => withAndroidManifest.ts} | 12 ++-- ... => withChangeNativeAndroidTextToWhite.ts} | 10 ++-- ...Media3Dash.js => withExcludeMedia3Dash.ts} | 6 +- plugins/{withGitPod.js => withGitPod.ts} | 14 ++++- ...eProperties.js => withGradleProperties.ts} | 19 +++++-- ...{withTVOSAppIcon.js => withTVOSAppIcon.ts} | 6 +- ...ithTVOSTopShelf.js => withTVOSTopShelf.ts} | 55 +++++++++++------- ...rManagement.js => withTVUserManagement.ts} | 6 +- .../{withTVXcodeEnv.js => withTVXcodeEnv.ts} | 14 ++--- ...stLocalCerts.js => withTrustLocalCerts.ts} | 23 ++++++-- utils/version.ts | 2 +- 19 files changed, 216 insertions(+), 89 deletions(-) rename app.config.js => app.config.ts (76%) rename plugins/{with-runtime-framework-headers.js => with-runtime-framework-headers.ts} (96%) rename plugins/{withAndroidAlertColors.js => withAndroidAlertColors.ts} (60%) rename plugins/{withAndroidManifest.js => withAndroidManifest.ts} (77%) rename plugins/{withChangeNativeAndroidTextToWhite.js => withChangeNativeAndroidTextToWhite.ts} (68%) rename plugins/{withExcludeMedia3Dash.js => withExcludeMedia3Dash.ts} (85%) rename plugins/{withGitPod.js => withGitPod.ts} (63%) rename plugins/{withGradleProperties.js => withGradleProperties.ts} (63%) rename plugins/{withTVOSAppIcon.js => withTVOSAppIcon.ts} (81%) rename plugins/{withTVOSTopShelf.js => withTVOSTopShelf.ts} (80%) rename plugins/{withTVUserManagement.js => withTVUserManagement.ts} (81%) rename plugins/{withTVXcodeEnv.js => withTVXcodeEnv.ts} (91%) rename plugins/{withTrustLocalCerts.js => withTrustLocalCerts.ts} (70%) diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml index c3fee61b8..8dd2da99a 100644 --- a/.github/workflows/build-apps.yml +++ b/.github/workflows/build-apps.yml @@ -11,7 +11,7 @@ on: push: branches: [develop, master] -# Exposed to `expo prebuild` (app.config.js → extra.build) so Settings can show the +# Exposed to `expo prebuild` (app.config.ts → extra.build) so Settings can show the # branch + commit + Actions run a CI build was made from. EAS cloud builds use # EAS_BUILD_* instead. The run number maps a sideloaded build back to its Actions # run (artifacts + logs) without needing Expo access. diff --git a/app.config.js b/app.config.ts similarity index 76% rename from app.config.js rename to app.config.ts index d29ddc324..ac2b433ab 100644 --- a/app.config.js +++ b/app.config.ts @@ -1,9 +1,13 @@ -const { execFileSync } = require("node:child_process"); +// Registers the tsx require hook so the TypeScript config plugins referenced +// from app.json ("./plugins/*.ts") can be loaded by Node during config evaluation. +import "tsx/cjs"; +import { execFileSync } from "node:child_process"; +import type { ConfigContext, ExpoConfig } from "expo/config"; // Build metadata, injected into `extra.build` and read at runtime via // expo-constants (see utils/version.ts). Sources in priority order: // EAS cloud build → GitHub Actions → explicit EXPO_PUBLIC_* → local git → null. -const git = (args) => { +const git = (args: string[]): string | null => { try { return execFileSync("git", args, { stdio: ["ignore", "pipe", "ignore"] }) .toString() @@ -42,16 +46,16 @@ const buildMeta = { builtAt: new Date().toISOString(), }; -module.exports = ({ config }) => { +export default ({ config }: ConfigContext): ExpoConfig => { if (process.env.EXPO_TV !== "1") { - config.plugins.push("expo-background-task"); + config.plugins?.push("expo-background-task"); - config.plugins.push([ + config.plugins?.push([ "react-native-google-cast", { useDefaultExpandedMediaControls: true }, ]); - config.plugins.push([ + config.plugins?.push([ "expo-camera", { cameraPermission: @@ -61,7 +65,7 @@ module.exports = ({ config }) => { } // Only override googleServicesFile if env var is set - const androidConfig = {}; + const androidConfig: { googleServicesFile?: string } = {}; if (process.env.GOOGLE_SERVICES_JSON) { androidConfig.googleServicesFile = process.env.GOOGLE_SERVICES_JSON; } @@ -71,5 +75,5 @@ module.exports = ({ config }) => { return { ...(Object.keys(androidConfig).length > 0 && { android: androidConfig }), ...config, - }; + } as ExpoConfig; }; diff --git a/app.json b/app.json index 296d674de..c17bfe806 100644 --- a/app.json +++ b/app.json @@ -71,8 +71,8 @@ ], "expo-router", "expo-font", - "./plugins/withExcludeMedia3Dash.js", - "./plugins/withTVUserManagement.js", + "./plugins/withExcludeMedia3Dash.ts", + "./plugins/withTVUserManagement.ts", [ "expo-build-properties", { @@ -131,17 +131,17 @@ } ], "expo-web-browser", - ["./plugins/with-runtime-framework-headers.js"], - ["./plugins/withChangeNativeAndroidTextToWhite.js"], - ["./plugins/withAndroidAlertColors.js"], - ["./plugins/withAndroidManifest.js"], - ["./plugins/withTrustLocalCerts.js"], - ["./plugins/withGradleProperties.js"], - ["./plugins/withTVOSAppIcon.js"], - ["./plugins/withTVOSTopShelf.js"], - ["./plugins/withTVXcodeEnv.js"], + ["./plugins/with-runtime-framework-headers.ts"], + ["./plugins/withChangeNativeAndroidTextToWhite.ts"], + ["./plugins/withAndroidAlertColors.ts"], + ["./plugins/withAndroidManifest.ts"], + ["./plugins/withTrustLocalCerts.ts"], + ["./plugins/withGradleProperties.ts"], + ["./plugins/withTVOSAppIcon.ts"], + ["./plugins/withTVOSTopShelf.ts"], + ["./plugins/withTVXcodeEnv.ts"], [ - "./plugins/withGitPod.js", + "./plugins/withGitPod.ts", { "podName": "MPVKit", "podspecUrl": "https://raw.githubusercontent.com/mpv-ios/MPVKit/0.41.0-av/MPVKit.podspec" diff --git a/bun.lock b/bun.lock index 97ba4fa23..e14a44cc1 100644 --- a/bun.lock +++ b/bun.lock @@ -112,6 +112,7 @@ "husky": "9.1.7", "lint-staged": "17.0.5", "react-test-renderer": "19.2.3", + "tsx": "^4.22.4", "typescript": "5.9.3", }, }, @@ -293,6 +294,58 @@ "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], + "@expo-google-fonts/material-symbols": ["@expo-google-fonts/material-symbols@0.4.38", "", {}, "sha512-IJkBtN1o8u9BW5fvSii1MyHPQ7Q0HxbWcVBvOrOzgMLpVtZw7R2w94wBTVR7kZwv3w1JNTESMmLA5Sqn1+Z36A=="], "@expo/cli": ["@expo/cli@56.1.12", "", { "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~56.0.9", "@expo/config-plugins": "~56.0.8", "@expo/devcert": "^1.2.1", "@expo/env": "~2.3.0", "@expo/image-utils": "^0.10.1", "@expo/inline-modules": "^0.0.10", "@expo/json-file": "^10.2.0", "@expo/log-box": "^56.0.12", "@expo/metro": "~56.0.0", "@expo/metro-config": "~56.0.13", "@expo/metro-file-map": "^56.0.3", "@expo/osascript": "^2.6.0", "@expo/package-manager": "^1.12.0", "@expo/plist": "^0.7.0", "@expo/prebuild-config": "^56.0.13", "@expo/require-utils": "^56.1.3", "@expo/router-server": "^56.0.12", "@expo/schema-utils": "^56.0.0", "@expo/spawn-async": "^1.8.0", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.4.4", "@react-native/dev-middleware": "0.85.3", "accepts": "^1.3.8", "arg": "^5.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.4", "expo-server": "^56.0.4", "fetch-nodeshim": "^0.4.10", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.1", "multitars": "^1.0.0", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^4.0.4", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "terminal-link": "^2.1.1", "toqr": "^0.1.1", "wrap-ansi": "^7.0.0", "ws": "^8.12.1", "zod": "^3.25.76" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "main.js" } }, "sha512-Ya/13E1yDx1oAuPw5MDmqzIGyzwSs7KSr1EjgSObOF0VO0GD9jqJjvjOiwurjScLUfxcGZQgq23UzMlBVHwdvA=="], @@ -917,6 +970,8 @@ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], @@ -1819,6 +1874,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.22.4", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="], + "type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="], "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], diff --git a/docs/tv-discovery.md b/docs/tv-discovery.md index b1a551656..c2bc67eb5 100644 --- a/docs/tv-discovery.md +++ b/docs/tv-discovery.md @@ -28,7 +28,7 @@ Apple TV uses a Top Shelf extension target, not the main app process. Relevant files: -- [plugins/withTVOSTopShelf.js](../plugins/withTVOSTopShelf.js) +- [plugins/withTVOSTopShelf.ts](../plugins/withTVOSTopShelf.ts) - [targets/StreamyfinTopShelf/TopShelfProvider.swift](../targets/StreamyfinTopShelf/TopShelfProvider.swift) - [modules/top-shelf-cache/ios/TopShelfCacheModule.swift](../modules/top-shelf-cache/ios/TopShelfCacheModule.swift) - [utils/topshelf/cache.ts](../utils/topshelf/cache.ts) diff --git a/package.json b/package.json index 588ada9ae..ce57aab3e 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "husky": "9.1.7", "lint-staged": "17.0.5", "react-test-renderer": "19.2.3", + "tsx": "^4.22.4", "typescript": "5.9.3" }, "expo": { diff --git a/plugins/with-runtime-framework-headers.js b/plugins/with-runtime-framework-headers.ts similarity index 96% rename from plugins/with-runtime-framework-headers.js rename to plugins/with-runtime-framework-headers.ts index 8405239b8..43e340469 100644 --- a/plugins/with-runtime-framework-headers.js +++ b/plugins/with-runtime-framework-headers.ts @@ -1,4 +1,4 @@ -const { withPodfile } = require("expo/config-plugins"); +import { type ConfigPlugin, withPodfile } from "expo/config-plugins"; const PATCH_START = "## >>> runtime-framework headers"; const PATCH_END = "## <<< runtime-framework headers"; @@ -13,7 +13,7 @@ const EXTRA_HDRS = [ `\${PODS_CONFIGURATION_BUILD_DIR}/React-rendererconsistency/React_rendererconsistency.framework/Headers`, ]; -function buildPatch() { +function buildPatch(): string { return [ PATCH_START, " extra_hdrs = [", @@ -91,7 +91,7 @@ function buildPatch() { ].join("\n"); } -module.exports = function withRuntimeFrameworkHeaders(config) { +const withRuntimeFrameworkHeaders: ConfigPlugin = (config) => { return withPodfile(config, (config) => { let podfile = config.modResults.contents; @@ -125,3 +125,5 @@ end return config; }); }; + +export default withRuntimeFrameworkHeaders; diff --git a/plugins/withAndroidAlertColors.js b/plugins/withAndroidAlertColors.ts similarity index 60% rename from plugins/withAndroidAlertColors.js rename to plugins/withAndroidAlertColors.ts index c0570047b..829b9a54b 100644 --- a/plugins/withAndroidAlertColors.js +++ b/plugins/withAndroidAlertColors.ts @@ -1,10 +1,20 @@ -const { +import { + type ConfigPlugin, withAndroidColors, withAndroidColorsNight, -} = require("expo/config-plugins"); +} from "expo/config-plugins"; -const withAndroidAlertColors = (config) => { - const setColor = (colorsList, name, value) => { +interface ColorResourceItem { + $: { name: string }; + _: string; +} + +const withAndroidAlertColors: ConfigPlugin = (config) => { + const setColor = ( + colorsList: ColorResourceItem[], + name: string, + value: string, + ) => { const existingColor = colorsList.find( (item) => item.$ && item.$.name === name, ); @@ -20,7 +30,7 @@ const withAndroidAlertColors = (config) => { config = withAndroidColors(config, (config) => { const colors = config.modResults; - const colorsList = colors.resources.color || []; + const colorsList = (colors.resources.color ?? []) as ColorResourceItem[]; setColor(colorsList, "colorPrimary", "#000000"); colors.resources.color = colorsList; return config; @@ -28,7 +38,7 @@ const withAndroidAlertColors = (config) => { config = withAndroidColorsNight(config, (config) => { const colors = config.modResults; - const colorsList = colors.resources.color || []; + const colorsList = (colors.resources.color ?? []) as ColorResourceItem[]; setColor(colorsList, "colorPrimary", "#FFFFFF"); colors.resources.color = colorsList; return config; @@ -37,4 +47,4 @@ const withAndroidAlertColors = (config) => { return config; }; -module.exports = withAndroidAlertColors; +export default withAndroidAlertColors; diff --git a/plugins/withAndroidManifest.js b/plugins/withAndroidManifest.ts similarity index 77% rename from plugins/withAndroidManifest.js rename to plugins/withAndroidManifest.ts index 883869fb5..7d5a9e62d 100644 --- a/plugins/withAndroidManifest.js +++ b/plugins/withAndroidManifest.ts @@ -1,8 +1,12 @@ -const { withAndroidManifest } = require("expo/config-plugins"); +import { type ConfigPlugin, withAndroidManifest } from "expo/config-plugins"; -const _withGoogleCastAndroidManifest = (config) => +const withGoogleCastAndroidManifest: ConfigPlugin = (config) => withAndroidManifest(config, async (mod) => { - const mainApplication = mod.modResults.manifest.application[0]; + const mainApplication = mod.modResults.manifest.application?.[0]; + + if (!mainApplication) { + return mod; + } // Initialize activity array if it doesn't exist if (!mainApplication.activity) { @@ -39,4 +43,4 @@ const _withGoogleCastAndroidManifest = (config) => return mod; }); -module.exports = _withGoogleCastAndroidManifest; +export default withGoogleCastAndroidManifest; diff --git a/plugins/withChangeNativeAndroidTextToWhite.js b/plugins/withChangeNativeAndroidTextToWhite.ts similarity index 68% rename from plugins/withChangeNativeAndroidTextToWhite.js rename to plugins/withChangeNativeAndroidTextToWhite.ts index efdb782b0..1ae4108ad 100644 --- a/plugins/withChangeNativeAndroidTextToWhite.js +++ b/plugins/withChangeNativeAndroidTextToWhite.ts @@ -1,8 +1,8 @@ -const { readFileSync, writeFileSync } = require("node:fs"); -const { join } = require("node:path"); -const { withDangerousMod } = require("expo/config-plugins"); +import { readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { type ConfigPlugin, withDangerousMod } from "expo/config-plugins"; -const withChangeNativeAndroidTextToWhite = (expoConfig) => +const withChangeNativeAndroidTextToWhite: ConfigPlugin = (expoConfig) => withDangerousMod(expoConfig, [ "android", (modConfig) => { @@ -30,4 +30,4 @@ const withChangeNativeAndroidTextToWhite = (expoConfig) => }, ]); -module.exports = withChangeNativeAndroidTextToWhite; +export default withChangeNativeAndroidTextToWhite; diff --git a/plugins/withExcludeMedia3Dash.js b/plugins/withExcludeMedia3Dash.ts similarity index 85% rename from plugins/withExcludeMedia3Dash.js rename to plugins/withExcludeMedia3Dash.ts index 8884cccf8..e6b936561 100644 --- a/plugins/withExcludeMedia3Dash.js +++ b/plugins/withExcludeMedia3Dash.ts @@ -1,6 +1,6 @@ -const { withAppBuildGradle } = require("expo/config-plugins"); +import { type ConfigPlugin, withAppBuildGradle } from "expo/config-plugins"; -module.exports = function withExcludeMedia3Dash(config) { +const withExcludeMedia3Dash: ConfigPlugin = (config) => { return withAppBuildGradle(config, (config) => { const contents = config.modResults.contents; @@ -32,3 +32,5 @@ configurations.all { return config; }); }; + +export default withExcludeMedia3Dash; diff --git a/plugins/withGitPod.js b/plugins/withGitPod.ts similarity index 63% rename from plugins/withGitPod.js rename to plugins/withGitPod.ts index dc046e8ac..5d5aab732 100644 --- a/plugins/withGitPod.js +++ b/plugins/withGitPod.ts @@ -1,6 +1,14 @@ -const { withPodfile } = require("@expo/config-plugins"); +import { type ConfigPlugin, withPodfile } from "expo/config-plugins"; -const withGitPod = (config, { podName, podspecUrl }) => { +interface GitPodOptions { + podName: string; + podspecUrl: string; +} + +const withGitPod: ConfigPlugin = ( + config, + { podName, podspecUrl }, +) => { return withPodfile(config, (config) => { const podfile = config.modResults.contents; @@ -21,4 +29,4 @@ const withGitPod = (config, { podName, podspecUrl }) => { }); }; -module.exports = withGitPod; +export default withGitPod; diff --git a/plugins/withGradleProperties.js b/plugins/withGradleProperties.ts similarity index 63% rename from plugins/withGradleProperties.js rename to plugins/withGradleProperties.ts index 23e4e34f7..0f1e37d1d 100644 --- a/plugins/withGradleProperties.js +++ b/plugins/withGradleProperties.ts @@ -1,12 +1,21 @@ -const { withGradleProperties } = require("expo/config-plugins"); +import type { ExpoConfig } from "expo/config"; +import { + AndroidConfig, + type ConfigPlugin, + withGradleProperties, +} from "expo/config-plugins"; -function setGradlePropertiesValue(config, key, value) { +function setGradlePropertiesValue( + config: ExpoConfig, + key: string, + value: string, +): ExpoConfig { return withGradleProperties(config, (exportedConfig) => { const props = exportedConfig.modResults; const keyIdx = props.findIndex( (item) => item.type === "property" && item.key === key, ); - const property = { + const property: AndroidConfig.Properties.PropertiesItem = { type: "property", key, value, @@ -22,7 +31,7 @@ function setGradlePropertiesValue(config, key, value) { }); } -module.exports = function withCustomPlugin(config) { +const withCustomGradleProperties: ConfigPlugin = (config) => { // Expo 52 is not setting this // https://github.com/expo/expo/issues/32558 config = setGradlePropertiesValue(config, "android.enableJetifier", "true"); @@ -35,3 +44,5 @@ module.exports = function withCustomPlugin(config) { ); return config; }; + +export default withCustomGradleProperties; diff --git a/plugins/withTVOSAppIcon.js b/plugins/withTVOSAppIcon.ts similarity index 81% rename from plugins/withTVOSAppIcon.js rename to plugins/withTVOSAppIcon.ts index 50114eb6b..1fadbac22 100644 --- a/plugins/withTVOSAppIcon.js +++ b/plugins/withTVOSAppIcon.ts @@ -1,6 +1,6 @@ -const { withXcodeProject } = require("@expo/config-plugins"); +import { type ConfigPlugin, withXcodeProject } from "expo/config-plugins"; -const withTVOSAppIcon = (config) => { +const withTVOSAppIcon: ConfigPlugin = (config) => { // Only apply for TV builds if (process.env.EXPO_TV !== "1") { return config; @@ -28,4 +28,4 @@ const withTVOSAppIcon = (config) => { }); }; -module.exports = withTVOSAppIcon; +export default withTVOSAppIcon; diff --git a/plugins/withTVOSTopShelf.js b/plugins/withTVOSTopShelf.ts similarity index 80% rename from plugins/withTVOSTopShelf.js rename to plugins/withTVOSTopShelf.ts index 56610fcf7..5fcd13742 100644 --- a/plugins/withTVOSTopShelf.js +++ b/plugins/withTVOSTopShelf.ts @@ -1,8 +1,10 @@ -const { +import type { ExpoConfig } from "expo/config"; +import { + type ConfigPlugin, withEntitlementsPlist, withInfoPlist, withXcodeProject, -} = require("@expo/config-plugins"); +} from "expo/config-plugins"; const EXTENSION_TARGET_NAME = "StreamyfinTopShelf"; const TARGET_SOURCE_DIR = "../targets/StreamyfinTopShelf"; @@ -10,19 +12,29 @@ const APP_GROUP_INFO_PLIST_KEY = "StreamyfinAppGroupIdentifier"; const KEYCHAIN_ACCESS_GROUP_INFO_PLIST_KEY = "StreamyfinKeychainAccessGroupIdentifier"; -function getBundleIdentifier(config) { +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) { +function getAppGroupIdentifier(config: ExpoConfig): string { return `group.${getBundleIdentifier(config)}.tvtopshelf`; } -function getKeychainAccessGroupIdentifier(config) { +function getKeychainAccessGroupIdentifier(config: ExpoConfig): string { return `$(AppIdentifierPrefix)${getBundleIdentifier(config)}`; } -function getBuildConfigurations(project, configurationListId) { +// 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]; @@ -30,18 +42,21 @@ function getBuildConfigurations(project, configurationListId) { const buildConfigurations = project.pbxXCBuildConfigurationSection(); return configurationList.buildConfigurations - .map((config) => buildConfigurations[config.value]) + .map((config: { value: string }) => buildConfigurations[config.value]) .filter(Boolean); } -function ensureAppGroup(value, appGroupIdentifier) { +function ensureAppGroup(value: unknown, appGroupIdentifier: string): string[] { const groups = Array.isArray(value) ? value : []; return groups.includes(appGroupIdentifier) ? groups : [...groups, appGroupIdentifier]; } -function ensureKeychainAccessGroup(value, keychainAccessGroupIdentifier) { +function ensureKeychainAccessGroup( + value: unknown, + keychainAccessGroupIdentifier: string, +): string[] { const groups = Array.isArray(value) ? value : []; return groups.includes(keychainAccessGroupIdentifier) ? groups @@ -49,13 +64,13 @@ function ensureKeychainAccessGroup(value, keychainAccessGroupIdentifier) { } function ensureAppExtension( - appExtensions, - targetName, - bundleIdentifier, - appGroupIdentifier, - keychainAccessGroupIdentifier, -) { - const extensionConfig = { + appExtensions: unknown, + targetName: string, + bundleIdentifier: string, + appGroupIdentifier: string, + keychainAccessGroupIdentifier: string, +): AppExtensionConfig[] { + const extensionConfig: AppExtensionConfig = { targetName, bundleIdentifier, entitlements: { @@ -63,7 +78,9 @@ function ensureAppExtension( "keychain-access-groups": [keychainAccessGroupIdentifier], }, }; - const extensions = Array.isArray(appExtensions) ? appExtensions : []; + 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, @@ -78,7 +95,7 @@ function ensureAppExtension( ); } -const withTVOSTopShelf = (config) => { +const withTVOSTopShelf: ConfigPlugin = (config) => { const appGroupIdentifier = getAppGroupIdentifier(config); const keychainAccessGroupIdentifier = getKeychainAccessGroupIdentifier(config); @@ -193,4 +210,4 @@ const withTVOSTopShelf = (config) => { }); }; -module.exports = withTVOSTopShelf; +export default withTVOSTopShelf; diff --git a/plugins/withTVUserManagement.js b/plugins/withTVUserManagement.ts similarity index 81% rename from plugins/withTVUserManagement.js rename to plugins/withTVUserManagement.ts index 651ee7385..f69f47b16 100644 --- a/plugins/withTVUserManagement.js +++ b/plugins/withTVUserManagement.ts @@ -1,9 +1,9 @@ -const { withEntitlementsPlist } = require("expo/config-plugins"); +import { type ConfigPlugin, withEntitlementsPlist } from "expo/config-plugins"; /** * Expo config plugin to add User Management entitlement for tvOS profile linking */ -const withTVUserManagement = (config) => { +const withTVUserManagement: ConfigPlugin = (config) => { // Only add for tvOS builds. The entitlement is restricted by Apple and must // be present in the provisioning profile, so injecting it into mobile builds // breaks signing ("Entitlement ... not found and could not be included in @@ -24,4 +24,4 @@ const withTVUserManagement = (config) => { }); }; -module.exports = withTVUserManagement; +export default withTVUserManagement; diff --git a/plugins/withTVXcodeEnv.js b/plugins/withTVXcodeEnv.ts similarity index 91% rename from plugins/withTVXcodeEnv.js rename to plugins/withTVXcodeEnv.ts index 86f367552..dd26e8f04 100644 --- a/plugins/withTVXcodeEnv.js +++ b/plugins/withTVXcodeEnv.ts @@ -1,7 +1,7 @@ -const { withDangerousMod } = require("@expo/config-plugins"); -const { execSync } = require("node:child_process"); -const fs = require("node:fs"); -const path = require("node:path"); +import { execSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { type ConfigPlugin, withDangerousMod } from "expo/config-plugins"; /** * Expo config plugin that adds EXPO_TV=1 and NODE_BINARY to .xcode.env.local for TV builds. @@ -12,7 +12,7 @@ const path = require("node:path"); * * It also sets NODE_BINARY for nvm users since Xcode can't resolve shell functions. */ -const withTVXcodeEnv = (config) => { +const withTVXcodeEnv: ConfigPlugin = (config) => { // Only apply for TV builds if (process.env.EXPO_TV !== "1") { return config; @@ -70,7 +70,7 @@ const withTVXcodeEnv = (config) => { /** * Get the actual node binary path, handling nvm installations. */ -function getNodeBinaryPath() { +function getNodeBinaryPath(): string | null { try { // First try to get node path directly (works for non-nvm installs) const directPath = execSync("which node 2>/dev/null", { @@ -114,4 +114,4 @@ function getNodeBinaryPath() { return null; } -module.exports = withTVXcodeEnv; +export default withTVXcodeEnv; diff --git a/plugins/withTrustLocalCerts.js b/plugins/withTrustLocalCerts.ts similarity index 70% rename from plugins/withTrustLocalCerts.js rename to plugins/withTrustLocalCerts.ts index 20e902e45..c9e69c38a 100644 --- a/plugins/withTrustLocalCerts.js +++ b/plugins/withTrustLocalCerts.ts @@ -1,18 +1,29 @@ -const { AndroidConfig, withAndroidManifest } = require("expo/config-plugins"); -const path = require("node:path"); -const fs = require("node:fs"); +import fs from "node:fs"; +import path from "node:path"; +import { + AndroidConfig, + type ConfigPlugin, + type ExportedConfigWithProps, + withAndroidManifest, +} from "expo/config-plugins"; + const fsPromises = fs.promises; const { getMainApplicationOrThrow } = AndroidConfig.Manifest; -const withTrustLocalCerts = (config) => { +type AndroidManifest = AndroidConfig.Manifest.AndroidManifest; + +const withTrustLocalCerts: ConfigPlugin = (config) => { return withAndroidManifest(config, async (mod) => { mod.modResults = await setCustomConfigAsync(mod, mod.modResults); return mod; }); }; -async function setCustomConfigAsync(config, androidManifest) { +async function setCustomConfigAsync( + config: ExportedConfigWithProps, + androidManifest: AndroidManifest, +): Promise { const src_file_path = path.join(__dirname, "network_security_config.xml"); const res_file_path = path.join( await AndroidConfig.Paths.getResourceFolderAsync( @@ -45,4 +56,4 @@ async function setCustomConfigAsync(config, androidManifest) { return androidManifest; } -module.exports = withTrustLocalCerts; +export default withTrustLocalCerts; diff --git a/utils/version.ts b/utils/version.ts index 812665c40..ade6025a7 100644 --- a/utils/version.ts +++ b/utils/version.ts @@ -5,7 +5,7 @@ import Constants from "expo-constants"; * clientInfo auto-tracks the app version instead of a hardcoded string. */ export const APP_VERSION = Application.nativeApplicationVersion ?? "unknown"; -/** Build metadata injected at build time by `app.config.js` into `extra.build`. */ +/** Build metadata injected at build time by `app.config.ts` into `extra.build`. */ export interface BuildMeta { commit?: string | null; branch?: string | null;