From 8c635498e0575f1db66254e883d03124b63b0cdb Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 26 Feb 2026 10:14:00 +0000 Subject: [PATCH 01/19] fix(expo-widgets): Remove extraneous dependencies (#43452) # Why - `@expo/config-plugins` isn't a necessary import; replace with `expo/config-plugins` like the other files - Remove `@expo/config-types` import; `expo/config` should be used - Open up `@expo/ui` dependency range to include patch versions, since we can't guarantee alignment on the user's side # Test Plan - n/a; should build as expected # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- packages/expo-widgets/CHANGELOG.md | 2 ++ packages/expo-widgets/package.json | 4 +--- packages/expo-widgets/plugin/build/withIosWarning.d.ts | 2 +- packages/expo-widgets/plugin/build/withIosWarning.js | 2 +- packages/expo-widgets/plugin/src/withIosWarning.ts | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/expo-widgets/CHANGELOG.md b/packages/expo-widgets/CHANGELOG.md index be80e059d806fa..a51ffa750cd3f6 100644 --- a/packages/expo-widgets/CHANGELOG.md +++ b/packages/expo-widgets/CHANGELOG.md @@ -12,6 +12,8 @@ ### šŸ’” Others +- Remove extraneous `@expo/config-plugins` dependency ([#43452](https://github.com/expo/expo/pull/43452) by [@kitten](https://github.com/kitten)) + ## 55.0.1 — 2026-02-25 ### šŸ› Bug fixes diff --git a/packages/expo-widgets/package.json b/packages/expo-widgets/package.json index 6fe797783ec1e8..a594d548e4816c 100644 --- a/packages/expo-widgets/package.json +++ b/packages/expo-widgets/package.json @@ -32,10 +32,8 @@ "license": "MIT", "homepage": "https://docs.expo.dev/versions/latest/sdk/widgets/", "dependencies": { - "@expo/config-plugins": "~55.0.6", - "@expo/config-types": "^55.0.5", "@expo/plist": "^0.5.2", - "@expo/ui": "55.0.1" + "@expo/ui": "~55.0.1" }, "devDependencies": { "expo-module-scripts": "^55.0.2" diff --git a/packages/expo-widgets/plugin/build/withIosWarning.d.ts b/packages/expo-widgets/plugin/build/withIosWarning.d.ts index 41d6b606b759ad..dd18bb0f54024c 100644 --- a/packages/expo-widgets/plugin/build/withIosWarning.d.ts +++ b/packages/expo-widgets/plugin/build/withIosWarning.d.ts @@ -1,4 +1,4 @@ -import { ConfigPlugin } from '@expo/config-plugins'; +import { ConfigPlugin } from 'expo/config-plugins'; type WithIosWarningProps = { property: string; warning: string; diff --git a/packages/expo-widgets/plugin/build/withIosWarning.js b/packages/expo-widgets/plugin/build/withIosWarning.js index b482192736931b..d3d492103aa614 100644 --- a/packages/expo-widgets/plugin/build/withIosWarning.js +++ b/packages/expo-widgets/plugin/build/withIosWarning.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const config_plugins_1 = require("@expo/config-plugins"); +const config_plugins_1 = require("expo/config-plugins"); // Hack to display the warning only once const withIosWarning = (config, { property, warning }) => (0, config_plugins_1.withInfoPlist)(config, (config) => { config_plugins_1.WarningAggregator.addWarningIOS(property, warning); diff --git a/packages/expo-widgets/plugin/src/withIosWarning.ts b/packages/expo-widgets/plugin/src/withIosWarning.ts index 4d62f7c5e4cf44..a0c8b3ed60e517 100644 --- a/packages/expo-widgets/plugin/src/withIosWarning.ts +++ b/packages/expo-widgets/plugin/src/withIosWarning.ts @@ -1,5 +1,5 @@ -import { ConfigPlugin, WarningAggregator, withInfoPlist } from '@expo/config-plugins'; -import { ExpoConfig } from '@expo/config-types'; +import type { ExpoConfig } from 'expo/config'; +import { ConfigPlugin, WarningAggregator, withInfoPlist } from 'expo/config-plugins'; type WithIosWarningProps = { property: string; warning: string }; From c0980ea5717328d89b06cd698e3909a46712c27a Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 26 Feb 2026 10:14:24 +0000 Subject: [PATCH 02/19] fix(babel-preset-expo): Opt widget functions out of `react-compiler` changes (#43451) # Why While the `customOptOutDirectives` are marked as unstable, they're the most reliable and simple way to opt us out of the react-compiler modifications for widget functions. It seems like a low-risk and optimal way to not modify the widget functions, and seems appropriate since they're not meant to be components, instead being just functions that return Swift UI JSX. # How - Add `customOptOutDirectives` override to `react-compiler` Babel plugin options opting out `widget` directive # Test Plan - Unit test checks that the compiler changes don't happen # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- packages/babel-preset-expo/CHANGELOG.md | 2 ++ packages/babel-preset-expo/build/index.js | 6 +++- .../src/__tests__/widgets-plugin.test.ts | 34 +++++++++++++++---- packages/babel-preset-expo/src/index.ts | 6 +++- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/babel-preset-expo/CHANGELOG.md b/packages/babel-preset-expo/CHANGELOG.md index f652a3fe21251e..8ef346be8e6d0c 100644 --- a/packages/babel-preset-expo/CHANGELOG.md +++ b/packages/babel-preset-expo/CHANGELOG.md @@ -8,6 +8,8 @@ ### šŸ› Bug fixes +- Opt `"widget"` functions for `expo-widgets` out of react-compiler ([#43451](https://github.com/expo/expo/pull/43451) by [@kitten](https://github.com/kitten)) + ### šŸ’” Others ## 55.0.8 — 2026-02-25 diff --git a/packages/babel-preset-expo/build/index.js b/packages/babel-preset-expo/build/index.js index 407245b68310d8..0ffca6e1802215 100644 --- a/packages/babel-preset-expo/build/index.js +++ b/packages/babel-preset-expo/build/index.js @@ -81,6 +81,7 @@ function babelPresetExpo(api, options = {}) { !isServerEnv && // Give users the ability to opt-out of the feature, per-platform. platformOptions['react-compiler'] !== false) { + const reactCompilerOptions = platformOptions['react-compiler']; extraPlugins.push([ require('babel-plugin-react-compiler'), { @@ -90,7 +91,10 @@ function babelPresetExpo(api, options = {}) { ...(platformOptions['react-compiler']?.environment ?? {}), }, panicThreshold: isDev ? undefined : 'NONE', - ...platformOptions['react-compiler'], + ...reactCompilerOptions, + // See: https://github.com/facebook/react/blob/074d96b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts#L160-L163 + // We need to opt-out for our widgets, since they're stringified functions that output Swift UI JSX + customOptOutDirectives: [...(reactCompilerOptions?.customOptOutDirectives ?? []), 'widget'], }, ]); } diff --git a/packages/babel-preset-expo/src/__tests__/widgets-plugin.test.ts b/packages/babel-preset-expo/src/__tests__/widgets-plugin.test.ts index ae20ccc44726ed..7ae30acac76a99 100644 --- a/packages/babel-preset-expo/src/__tests__/widgets-plugin.test.ts +++ b/packages/babel-preset-expo/src/__tests__/widgets-plugin.test.ts @@ -31,12 +31,10 @@ const DEF_OPTIONS = { caller: getCaller({ ...ENABLED_CALLER }), }; -function transformTest(sourceCode: string) { - const options = { ...DEF_OPTIONS }; - +function transformTest(sourceCode: string, opts?: Partial) { + const options = { ...DEF_OPTIONS, ...opts }; const results = babel.transform(sourceCode, options); if (!results) throw new Error('Failed to transform code'); - return { code: results.code, }; @@ -68,7 +66,6 @@ describe('widgets-plugin', () => { describe('transform', () => { it('stringifies widget function after JSX transform', () => { - const backtick = '`'; const result = transformTest(` function MyComponent({ name }) { 'widget'; @@ -81,7 +78,6 @@ describe('widgets-plugin', () => { }); it('stringifies widget arrow function after JSX transform', () => { - const backtick = '`'; const result = transformTest(` const MyComponent = ({ name }) => { 'widget'; @@ -106,4 +102,30 @@ describe('widgets-plugin', () => { expect(result.code).toContain('jsx(_Fragment'); }); }); + + describe('react-compiler', () => { + const COMPILER_OPTS = { + caller: getCaller({ + ...ENABLED_CALLER, + supportsReactCompiler: true, + }), + }; + + it('stringifies widget function with react compiler enabled', () => { + const result = transformTest( + ` + function MyComponent({ name }) { + 'widget'; + return {name + \`sadaas\`}; + } + `, + COMPILER_OPTS + ); + + // Shouldn't add the compiler, since we're opting out + expect(result.code).not.toContain('react/compiler-runtime'); + expect(result.code).toContain('var MyComponent = `function'); + expect(result.code).toContain('jsx('); + }); + }); }); diff --git a/packages/babel-preset-expo/src/index.ts b/packages/babel-preset-expo/src/index.ts index 4473d0e19ca2d8..7d09600ab996f3 100644 --- a/packages/babel-preset-expo/src/index.ts +++ b/packages/babel-preset-expo/src/index.ts @@ -173,6 +173,7 @@ function babelPresetExpo(api: ConfigAPI, options: BabelPresetExpoOptions = {}): // Give users the ability to opt-out of the feature, per-platform. platformOptions['react-compiler'] !== false ) { + const reactCompilerOptions = platformOptions['react-compiler']; extraPlugins.push([ require('babel-plugin-react-compiler'), { @@ -182,7 +183,10 @@ function babelPresetExpo(api: ConfigAPI, options: BabelPresetExpoOptions = {}): ...(platformOptions['react-compiler']?.environment ?? {}), }, panicThreshold: isDev ? undefined : 'NONE', - ...platformOptions['react-compiler'], + ...reactCompilerOptions, + // See: https://github.com/facebook/react/blob/074d96b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts#L160-L163 + // We need to opt-out for our widgets, since they're stringified functions that output Swift UI JSX + customOptOutDirectives: [...(reactCompilerOptions?.customOptOutDirectives ?? []), 'widget'], }, ]); } From 7a44389ae840b4d6a673537cbd47af9a0d95d696 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:50:03 +0530 Subject: [PATCH 03/19] Bump minimatch from 3.1.2 to 3.1.3 in /docs (#43404) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index 4c7bce4e613e86..eb3c95ba0e92bf 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -10917,11 +10917,11 @@ __metadata: linkType: hard "minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" + version: 3.1.3 + resolution: "minimatch@npm:3.1.3" dependencies: brace-expansion: "npm:^1.1.7" - checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 + checksum: 10c0/c1ffce4be47e88df013f66f55176c25a93fdd8ad15735309cf1782f0433a02f363cee298f8763ceaaaf85e70ff7f30dc84a1a8d00a6fb6ca72032e5b51f9b89c languageName: node linkType: hard From 6070c099d2af799324457a57d7cfc91e84356220 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:51:35 +0530 Subject: [PATCH 04/19] Bump rollup from 4.46.2 to 4.59.0 in /docs (#43457) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/yarn.lock | 220 ++++++++++++++++++++++++++++++------------------- 1 file changed, 135 insertions(+), 85 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index eb3c95ba0e92bf..9b8fdb347cebcd 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -3395,142 +3395,177 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.46.2" +"@rollup/rollup-android-arm-eabi@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.59.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-android-arm64@npm:4.46.2" +"@rollup/rollup-android-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-android-arm64@npm:4.59.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-darwin-arm64@npm:4.46.2" +"@rollup/rollup-darwin-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.59.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-darwin-x64@npm:4.46.2" +"@rollup/rollup-darwin-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.59.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.46.2" +"@rollup/rollup-freebsd-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.59.0" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-freebsd-x64@npm:4.46.2" +"@rollup/rollup-freebsd-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-freebsd-x64@npm:4.59.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.46.2" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.46.2" +"@rollup/rollup-linux-arm-musleabihf@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.59.0" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.46.2" +"@rollup/rollup-linux-arm64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.59.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.46.2" +"@rollup/rollup-linux-arm64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.59.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.46.2" +"@rollup/rollup-linux-loong64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.59.0" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-gnu@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.46.2" +"@rollup/rollup-linux-loong64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.59.0" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.59.0" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.46.2" +"@rollup/rollup-linux-ppc64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.59.0" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.59.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.46.2" +"@rollup/rollup-linux-riscv64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.59.0" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.46.2" +"@rollup/rollup-linux-s390x-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.59.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.46.2" +"@rollup/rollup-linux-x64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.59.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.46.2" +"@rollup/rollup-linux-x64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.59.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.46.2" +"@rollup/rollup-openbsd-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-openbsd-x64@npm:4.59.0" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.59.0" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.59.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.46.2" +"@rollup/rollup-win32-ia32-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.59.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.46.2": - version: 4.46.2 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.46.2" +"@rollup/rollup-win32-x64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.59.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.59.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -13012,29 +13047,34 @@ __metadata: linkType: hard "rollup@npm:^4.35.0": - version: 4.46.2 - resolution: "rollup@npm:4.46.2" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.46.2" - "@rollup/rollup-android-arm64": "npm:4.46.2" - "@rollup/rollup-darwin-arm64": "npm:4.46.2" - "@rollup/rollup-darwin-x64": "npm:4.46.2" - "@rollup/rollup-freebsd-arm64": "npm:4.46.2" - "@rollup/rollup-freebsd-x64": "npm:4.46.2" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.46.2" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.46.2" - "@rollup/rollup-linux-arm64-gnu": "npm:4.46.2" - "@rollup/rollup-linux-arm64-musl": "npm:4.46.2" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.46.2" - "@rollup/rollup-linux-ppc64-gnu": "npm:4.46.2" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.46.2" - "@rollup/rollup-linux-riscv64-musl": "npm:4.46.2" - "@rollup/rollup-linux-s390x-gnu": "npm:4.46.2" - "@rollup/rollup-linux-x64-gnu": "npm:4.46.2" - "@rollup/rollup-linux-x64-musl": "npm:4.46.2" - "@rollup/rollup-win32-arm64-msvc": "npm:4.46.2" - "@rollup/rollup-win32-ia32-msvc": "npm:4.46.2" - "@rollup/rollup-win32-x64-msvc": "npm:4.46.2" + version: 4.59.0 + resolution: "rollup@npm:4.59.0" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.59.0" + "@rollup/rollup-android-arm64": "npm:4.59.0" + "@rollup/rollup-darwin-arm64": "npm:4.59.0" + "@rollup/rollup-darwin-x64": "npm:4.59.0" + "@rollup/rollup-freebsd-arm64": "npm:4.59.0" + "@rollup/rollup-freebsd-x64": "npm:4.59.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.59.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.59.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.59.0" + "@rollup/rollup-linux-loong64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-loong64-musl": "npm:4.59.0" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-ppc64-musl": "npm:4.59.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-riscv64-musl": "npm:4.59.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.59.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-x64-musl": "npm:4.59.0" + "@rollup/rollup-openbsd-x64": "npm:4.59.0" + "@rollup/rollup-openharmony-arm64": "npm:4.59.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.59.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.59.0" + "@rollup/rollup-win32-x64-gnu": "npm:4.59.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.59.0" "@types/estree": "npm:1.0.8" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -13058,10 +13098,14 @@ __metadata: optional: true "@rollup/rollup-linux-arm64-musl": optional: true - "@rollup/rollup-linux-loongarch64-gnu": + "@rollup/rollup-linux-loong64-gnu": + optional: true + "@rollup/rollup-linux-loong64-musl": optional: true "@rollup/rollup-linux-ppc64-gnu": optional: true + "@rollup/rollup-linux-ppc64-musl": + optional: true "@rollup/rollup-linux-riscv64-gnu": optional: true "@rollup/rollup-linux-riscv64-musl": @@ -13072,17 +13116,23 @@ __metadata: optional: true "@rollup/rollup-linux-x64-musl": optional: true + "@rollup/rollup-openbsd-x64": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true "@rollup/rollup-win32-arm64-msvc": optional: true "@rollup/rollup-win32-ia32-msvc": optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true "@rollup/rollup-win32-x64-msvc": optional: true fsevents: optional: true bin: rollup: dist/bin/rollup - checksum: 10c0/f428497fe119fe7c4e34f1020d45ba13e99b94c9aa36958d88823d932b155c9df3d84f53166f3ee913ff68ea6c7599a9ab34861d88562ad9d8420f64ca5dad4c + checksum: 10c0/f38742da34cfee5e899302615fa157aa77cb6a2a1495e5e3ce4cc9c540d3262e235bbe60caa31562bbfe492b01fdb3e7a8c43c39d842d3293bcf843123b766fc languageName: node linkType: hard From da69bc8c12f8725d43e2dc48c46992ad315c7895 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 26 Feb 2026 10:33:28 +0000 Subject: [PATCH 05/19] chore(expo-router,updates,tools): Remove unintended version pins (and update `tools` check) (#43456) Resolves #43448 # Why We almost never intend to pin versions, unless some very specific conditions make this safe. In most cases, pins are just unnecessary and lead to unneeded duplicates. In some conditions pins can be unsafe: - when users depend on the same library and the library has singleton state (or transitive, misaligned dependencies with singleton state) - when the dependency is a native module that is likely to get misaligned (without `autolinkingModuleResolution`) - when it's unreasonable or hard for people to match the version range (if it's transitive, it's unknown) - when it's a peer dependency (it's unlikely to easily share peers with pins) That's a non-exhaustive list. For now, a `tools` check is added to prevent most version pins, since it's easy for commands to accidentally add them. # How - Add tools check to prevent most version pinning - Remove pinned `react-navigation` pins in `expo-router` - Rmeove pinned `arg` version in `expo-updates` # Test Plan - CI should pass; Check local `expo-router` project against latest matching ranges (adjust range if needed to `~` or add an upper limit) # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- packages/@expo/cli/CHANGELOG.md | 2 + .../metro/createExpoAutolinkingResolver.ts | 3 + packages/expo-router/CHANGELOG.md | 2 + packages/expo-router/package.json | 8 +-- packages/expo-updates/CHANGELOG.md | 2 + packages/expo-updates/package.json | 2 +- .../check-packages/checkDependenciesAsync.ts | 60 ++++++++++++++++++- tools/src/commands/PromotePackages.ts | 6 +- tools/src/promote-packages/helpers.ts | 4 +- .../promote-packages/tasks/promotePackages.ts | 8 +-- .../publish-packages/tasks/publishPackages.ts | 4 +- yarn.lock | 18 +----- 12 files changed, 79 insertions(+), 40 deletions(-) diff --git a/packages/@expo/cli/CHANGELOG.md b/packages/@expo/cli/CHANGELOG.md index 9b97fc2669365e..dd159312a05912 100644 --- a/packages/@expo/cli/CHANGELOG.md +++ b/packages/@expo/cli/CHANGELOG.md @@ -10,6 +10,8 @@ ### šŸ’” Others +- Add `@react-navigation/core` and `@react-navigation/native` to autolinking resolution ([#43456](https://github.com/expo/expo/pull/43456) by [@kitten](https://github.com/kitten)) + ## 55.0.12 — 2026-02-25 _This version does not introduce any user-facing changes._ diff --git a/packages/@expo/cli/src/start/server/metro/createExpoAutolinkingResolver.ts b/packages/@expo/cli/src/start/server/metro/createExpoAutolinkingResolver.ts index 2a2ca7248ca65d..98a0d5a10edfef 100644 --- a/packages/@expo/cli/src/start/server/metro/createExpoAutolinkingResolver.ts +++ b/packages/@expo/cli/src/start/server/metro/createExpoAutolinkingResolver.ts @@ -30,6 +30,9 @@ const KNOWN_STICKY_DEPENDENCIES = [ // Peer dependencies from expo-router 'react-native-gesture-handler', 'react-native-reanimated', + // Has a context that needs to be deduplicated + '@react-navigation/core', + '@react-navigation/native', ]; const AUTOLINKING_PLATFORMS = ['android', 'ios', 'web'] as const; diff --git a/packages/expo-router/CHANGELOG.md b/packages/expo-router/CHANGELOG.md index 2ee3ff10587424..836ef914729b46 100644 --- a/packages/expo-router/CHANGELOG.md +++ b/packages/expo-router/CHANGELOG.md @@ -8,6 +8,8 @@ ### šŸ› Bug fixes +- Fix pinned `react-navigation` dependencies ([#43456](https://github.com/expo/expo/pull/43456) by [@kitten](https://github.com/kitten)) + ### šŸ’” Others ## 55.0.2 — 2026-02-25 diff --git a/packages/expo-router/package.json b/packages/expo-router/package.json index 3514308c541271..b201fe0b2415ae 100644 --- a/packages/expo-router/package.json +++ b/packages/expo-router/package.json @@ -135,11 +135,11 @@ "dependencies": { "@expo/metro-runtime": "^55.0.6", "@expo/schema-utils": "^55.0.2", - "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", - "@react-navigation/bottom-tabs": "7.10.1", - "@react-navigation/native": "7.1.28", - "@react-navigation/native-stack": "7.10.1", + "@react-navigation/bottom-tabs": "^7.10.1", + "@react-navigation/native": "^7.1.28", + "@react-navigation/native-stack": "^7.10.1", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", diff --git a/packages/expo-updates/CHANGELOG.md b/packages/expo-updates/CHANGELOG.md index ab0443f1b3c3c1..7dc2521dacbf84 100644 --- a/packages/expo-updates/CHANGELOG.md +++ b/packages/expo-updates/CHANGELOG.md @@ -10,6 +10,8 @@ ### šŸ’” Others +- Remove pin on `arg` dependency ([#43456](https://github.com/expo/expo/pull/43456) by [@kitten](https://github.com/kitten)) + ## 55.0.11 — 2026-02-25 _This version does not introduce any user-facing changes._ diff --git a/packages/expo-updates/package.json b/packages/expo-updates/package.json index 6c4dfd4fca8f8c..555736d2172cb8 100644 --- a/packages/expo-updates/package.json +++ b/packages/expo-updates/package.json @@ -41,7 +41,7 @@ "@expo/code-signing-certificates": "^0.0.6", "@expo/plist": "^0.5.2", "@expo/spawn-async": "^1.7.2", - "arg": "4.1.0", + "arg": "^4.1.0", "chalk": "^4.1.2", "debug": "^4.3.4", "expo-eas-client": "~55.0.2", diff --git a/tools/src/check-packages/checkDependenciesAsync.ts b/tools/src/check-packages/checkDependenciesAsync.ts index 6e0f1f9cb38a24..4c6d7555eb1881 100644 --- a/tools/src/check-packages/checkDependenciesAsync.ts +++ b/tools/src/check-packages/checkDependenciesAsync.ts @@ -123,13 +123,15 @@ export async function checkDependenciesAsync(pkg: Package, type: PackageCheckTyp return; } - const getValidExternalImportKind = createExternalImportValidator(pkg); + const validator = createExternalImportValidator(pkg); let invalidImports: { file: SourceFile; importRef: SourceFileImportRef; kind: DependencyKind | undefined; }[] = []; + const invalidDependencyRanges: string[] = []; + for (const source of sources) { for (const importRef of source.importRefs) { if (importRef.type !== 'external' || pkg.packageName === importRef.packageName) { @@ -139,7 +141,10 @@ export async function checkDependenciesAsync(pkg: Package, type: PackageCheckTyp } else if (isIgnoredPackage) { continue; } - const kind = getValidExternalImportKind(importRef); + if (validator.isPinnedDependencyRange(importRef)) { + invalidDependencyRanges.push(importRef.packageName); + } + const kind = validator.getValidExternalImportKind(importRef); if (!kind || kind === DependencyKind.Dev) { invalidImports.push({ file: source.file, importRef, kind }); } @@ -194,6 +199,11 @@ export async function checkDependenciesAsync(pkg: Package, type: PackageCheckTyp throw new Error(`${pkg.packageName} has invalid dependency chains.`); } } + + if (invalidDependencyRanges.length) { + Logger.warn(`šŸ“¦ Risky versions: ${invalidDependencyRanges.join(', ')} are pinned!`); + throw new Error(`${pkg.packageName} has invalid pinned versions.`); + } } function isNCCBuilt(pkg: Package): boolean { @@ -221,7 +231,51 @@ function createExternalImportValidator(pkg: Package) { DependencyKind.Peer, ]); dependencies.forEach((dependency) => dependencyMap.set(dependency.name, dependency)); - return (ref: SourceFileImportRef) => dependencyMap.get(ref.packageName)?.kind; + const seenDependencyName = new Set(); + return { + getValidExternalImportKind(ref: SourceFileImportRef) { + return dependencyMap.get(ref.packageName)?.kind; + }, + isPinnedDependencyRange(ref: SourceFileImportRef) { + // List of exceptions: + if (pkg.packageName === 'patch-project' || pkg.packageName.startsWith('@expo/')) { + // Ignore this project + return null; + } else if (ref.packageName.startsWith('@expo/')) { + // Internal packages are ignored + return null; + } else if (ref.packageName.startsWith('@react-native/')) { + // Sub-deps on react-native, fine to pin + return null; + } else if (ref.packageName === 'xml2js') { + // TODO: Unpin + return null; + } else if (pkg.packageName === 'expo' && ref.packageName === 'expo-modules-core') { + // TODO: Exception, but there's potentially no need for this + return null; + } else if ( + pkg.packageName === 'expo-dev-client' && + (ref.packageName === 'expo-dev-launcher' || ref.packageName === 'expo-dev-menu') + ) { + // TODO: Unpin + return null; + } + + if (seenDependencyName.has(ref.packageName)) { + return null; + } + seenDependencyName.add(ref.packageName); + const dependency = dependencyMap.get(ref.packageName); + if (dependency && dependency.kind !== DependencyKind.Dev) { + // NOTE: Loose check to see if a dependency is pinned + const isLoose = + /[~|^><=](\s*\d+\.)/.test(dependency.versionRange) || dependency.versionRange === '*'; + const isPinned = /^\d+\.\d+\.\d+$/.test(dependency.versionRange); + return !isLoose || isPinned; + } + return null; + }, + }; } /** Get a list of all source files to validate for dependency chains */ diff --git a/tools/src/commands/PromotePackages.ts b/tools/src/commands/PromotePackages.ts index 61a64a39bf05a0..4f3bb97982df42 100644 --- a/tools/src/commands/PromotePackages.ts +++ b/tools/src/commands/PromotePackages.ts @@ -40,11 +40,7 @@ export default (program: Command) => { 'Lists packages with unpublished changes since the previous version.', false ) - .option( - '-r, --reverse', - 'Promote packages in reverse alphabetical order (Z to A).', - false - ) + .option('-r, --reverse', 'Promote packages in reverse alphabetical order (Z to A).', false) .option( '--prompt-otp', 'Prompt for an npm OTP code before promoting. Re-prompts automatically when the code expires.', diff --git a/tools/src/promote-packages/helpers.ts b/tools/src/promote-packages/helpers.ts index d8929879fe6c63..1baeac1a64f50c 100644 --- a/tools/src/promote-packages/helpers.ts +++ b/tools/src/promote-packages/helpers.ts @@ -54,9 +54,7 @@ function printPackagesToPromoteInternal(parcels: Parcel[], headerText: string): if (parcels.length > 0) { logger.log(' ', magenta(headerText)); - const sorted = [...parcels].sort((a, b) => - a.pkg.packageName.localeCompare(b.pkg.packageName) - ); + const sorted = [...parcels].sort((a, b) => a.pkg.packageName.localeCompare(b.pkg.packageName)); for (const { pkg, state } of sorted) { logger.log( diff --git a/tools/src/promote-packages/tasks/promotePackages.ts b/tools/src/promote-packages/tasks/promotePackages.ts index 5241cd4023d080..0659c1007f10e6 100644 --- a/tools/src/promote-packages/tasks/promotePackages.ts +++ b/tools/src/promote-packages/tasks/promotePackages.ts @@ -24,9 +24,7 @@ export const promotePackages = new Task( logger.info(`\nšŸš€ Promoting packages to ${yellow.bold(options.tag)} tag...`); // Sort alphabetically, optionally reversed. - const sorted = [...parcels].sort((a, b) => - a.pkg.packageName.localeCompare(b.pkg.packageName) - ); + const sorted = [...parcels].sort((a, b) => a.pkg.packageName.localeCompare(b.pkg.packageName)); if (options.reverse) { sorted.reverse(); } @@ -51,9 +49,7 @@ export const promotePackages = new Task( // Tag the local version of the package. if (!options.dry) { - await withOtpRetry(() => - Npm.addTagAsync(pkg.packageName, pkg.packageVersion, options.tag) - ); + await withOtpRetry(() => Npm.addTagAsync(pkg.packageName, pkg.packageVersion, options.tag)); } } diff --git a/tools/src/publish-packages/tasks/publishPackages.ts b/tools/src/publish-packages/tasks/publishPackages.ts index 18030673f5d89b..821fe07be9e768 100644 --- a/tools/src/publish-packages/tasks/publishPackages.ts +++ b/tools/src/publish-packages/tasks/publishPackages.ts @@ -73,9 +73,7 @@ export const publishPackages = new Task( logger.log(' ', `Assigning ${yellow(sdkTag)} tag to ${green(pkg.packageName)}`); if (!options.dry) { await sleepAsync(1000); // wait for npm to process the package - await withOtpRetry(() => - Npm.addTagAsync(pkg.packageName, releaseVersion, sdkTag) - ); + await withOtpRetry(() => Npm.addTagAsync(pkg.packageName, releaseVersion, sdkTag)); } } } catch (error) { diff --git a/yarn.lock b/yarn.lock index 7cf9bc6e5835b8..f414936cb665ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3530,14 +3530,7 @@ "@radix-ui/react-use-callback-ref" "1.1.1" "@radix-ui/react-use-controllable-state" "1.2.2" -"@radix-ui/react-slot@1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.0.tgz#57727fc186ddb40724ccfbe294e1a351d92462ba" - integrity sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w== - dependencies: - "@radix-ui/react-compose-refs" "1.1.2" - -"@radix-ui/react-slot@1.2.3": +"@radix-ui/react-slot@1.2.3", "@radix-ui/react-slot@^1.2.0": version "1.2.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz#502d6e354fc847d4169c3bc5f189de777f68cfe1" integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== @@ -3778,7 +3771,7 @@ invariant "^2.2.4" nullthrows "^1.1.1" -"@react-navigation/bottom-tabs@7.10.1", "@react-navigation/bottom-tabs@^7.3.10", "@react-navigation/bottom-tabs@^7.7.3": +"@react-navigation/bottom-tabs@7.10.1", "@react-navigation/bottom-tabs@^7.10.1", "@react-navigation/bottom-tabs@^7.3.10", "@react-navigation/bottom-tabs@^7.7.3": version "7.10.1" resolved "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.10.1.tgz#8d28e623f1f503c0e66b55837fa03a78a20ea2a8" integrity sha512-MirOzKEe/rRwPSE9HMrS4niIo0LyUhewlvd01TpzQ1ipuXjH2wJbzAM9gS/r62zriB6HMHz2OY6oIRduwQJtTw== @@ -3820,7 +3813,7 @@ use-latest-callback "^0.2.4" use-sync-external-store "^1.5.0" -"@react-navigation/native-stack@7.10.1": +"@react-navigation/native-stack@7.10.1", "@react-navigation/native-stack@^7.10.1": version "7.10.1" resolved "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.10.1.tgz#de6551286a5e9e2b0c466daa83e0f9bf0321815b" integrity sha512-8jt7olKysn07HuKKSjT/ahZZTV+WaZa96o9RI7gAwh7ATlUDY02rIRttwvCyjovhSjD9KCiuJ+Hd4kwLidHwJw== @@ -5345,11 +5338,6 @@ anymatch@^3.0.0, anymatch@^3.0.3, anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -arg@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0" - integrity sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg== - arg@5.0.2, arg@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" From 956567dca902c0bcaaf20074019d4ae9637fd93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosmaty?= Date: Thu, 26 Feb 2026 11:56:55 +0100 Subject: [PATCH 06/19] [core][Android] Inline `Permission` enum (#43364) --- .../universal/ScopedFilePermissionModule.kt | 1 - .../java/expo/modules/asset/AssetModule.kt | 6 +- .../modules/filesystem/FileSystemDirectory.kt | 22 ++++--- .../filesystem/FileSystemExceptions.kt | 4 +- .../expo/modules/filesystem/FileSystemFile.kt | 26 ++++---- .../modules/filesystem/FileSystemModule.kt | 6 +- .../expo/modules/filesystem/FileSystemPath.kt | 18 ++--- .../legacy/FileSystemLegacyModule.kt | 66 ++++++++++--------- .../interfaces/filesystem/Permission.java | 5 -- .../interfaces/filesystem/Permission.kt | 6 ++ .../kotlin/services/FilePermissionService.kt | 6 +- .../expo/modules/sharing/SharingModule.kt | 4 +- .../videothumbnails/VideoThumbnailsModule.kt | 5 +- 13 files changed, 94 insertions(+), 81 deletions(-) delete mode 100644 packages/expo-modules-core/android/src/main/java/expo/modules/interfaces/filesystem/Permission.java create mode 100644 packages/expo-modules-core/android/src/main/java/expo/modules/interfaces/filesystem/Permission.kt diff --git a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/ScopedFilePermissionModule.kt b/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/ScopedFilePermissionModule.kt index 9f7c68450eb05c..69eb1d974a4b8c 100644 --- a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/ScopedFilePermissionModule.kt +++ b/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/ScopedFilePermissionModule.kt @@ -1,6 +1,5 @@ package versioned.host.exp.exponent.modules.universal -import expo.modules.interfaces.filesystem.Permission import expo.modules.kotlin.services.FilePermissionService import host.exp.exponent.utils.ScopedContext import java.io.File diff --git a/packages/expo-asset/android/src/main/java/expo/modules/asset/AssetModule.kt b/packages/expo-asset/android/src/main/java/expo/modules/asset/AssetModule.kt index 682f0c19643202..b010e82cb8fe6d 100644 --- a/packages/expo-asset/android/src/main/java/expo/modules/asset/AssetModule.kt +++ b/packages/expo-asset/android/src/main/java/expo/modules/asset/AssetModule.kt @@ -3,13 +3,13 @@ package expo.modules.asset import android.content.Context import android.net.Uri import android.util.Log -import expo.modules.interfaces.filesystem.Permission import expo.modules.kotlin.AppContext import expo.modules.kotlin.exception.CodedException import expo.modules.kotlin.exception.Exceptions import expo.modules.kotlin.functions.Coroutine import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition +import expo.modules.kotlin.services.FilePermissionService import kotlinx.coroutines.withContext import java.io.File import java.io.FileInputStream @@ -50,7 +50,9 @@ class AssetModule : Module() { localUrl.mkdirs() } - if (!appContext.filePermission.getPathPermissions(context, requireNotNull(localUrl.parent)).contains(Permission.WRITE)) { + if (!appContext.filePermission.getPathPermissions(context, requireNotNull(localUrl.parent)) + .contains(FilePermissionService.Permission.WRITE) + ) { throw UnableToDownloadAssetException(uri.toString()) } diff --git a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemDirectory.kt b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemDirectory.kt index 48b545083d2764..f53487ba5726ec 100644 --- a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemDirectory.kt +++ b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemDirectory.kt @@ -1,7 +1,7 @@ package expo.modules.filesystem import android.net.Uri -import expo.modules.interfaces.filesystem.Permission +import expo.modules.kotlin.services.FilePermissionService class FileSystemDirectory(uri: Uri) : FileSystemPath(uri) { fun validatePath() { @@ -15,7 +15,7 @@ class FileSystemDirectory(uri: Uri) : FileSystemPath(uri) { } val exists: Boolean get() { - return if (checkPermission(Permission.READ)) { + return if (checkPermission(FilePermissionService.Permission.READ)) { file.isDirectory() } else { false @@ -23,14 +23,14 @@ class FileSystemDirectory(uri: Uri) : FileSystemPath(uri) { } val size: Long get() { - validatePermission(Permission.READ) + validatePermission(FilePermissionService.Permission.READ) validateType() return file.walkTopDown().filter { it.isFile() }.map { it.length() }.sum() } fun info(): DirectoryInfo { validateType() - validatePermission(Permission.READ) + validatePermission(FilePermissionService.Permission.READ) if (!file.exists()) { val directoryInfo = DirectoryInfo( exists = false, @@ -52,7 +52,7 @@ class FileSystemDirectory(uri: Uri) : FileSystemPath(uri) { fun create(options: CreateOptions = CreateOptions()) { validateType() - validatePermission(Permission.WRITE) + validatePermission(FilePermissionService.Permission.WRITE) if (!needsCreation(options)) { return } @@ -75,22 +75,24 @@ class FileSystemDirectory(uri: Uri) : FileSystemPath(uri) { fun createFile(mimeType: String?, fileName: String): FileSystemFile { validateType() - validatePermission(Permission.WRITE) - val newFile = file.createFile(mimeType ?: "text/plain", fileName) ?: throw UnableToCreateException("file could not be created") + validatePermission(FilePermissionService.Permission.WRITE) + val newFile = file.createFile(mimeType ?: "text/plain", fileName) + ?: throw UnableToCreateException("file could not be created") return FileSystemFile(newFile.uri) } fun createDirectory(fileName: String): FileSystemDirectory { validateType() - validatePermission(Permission.WRITE) - val newDirectory = file.createDirectory(fileName) ?: throw UnableToCreateException("directory could not be created") + validatePermission(FilePermissionService.Permission.WRITE) + val newDirectory = file.createDirectory(fileName) + ?: throw UnableToCreateException("directory could not be created") return FileSystemDirectory(newDirectory.uri) } // this function is internal and will be removed in the future (when returning arrays of shared objects is supported) fun listAsRecords(): List> { validateType() - validatePermission(Permission.READ) + validatePermission(FilePermissionService.Permission.READ) return file.listFilesAsUnified().map { val uriString = it.uri.toString() val isDir = it.isDirectory() diff --git a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemExceptions.kt b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemExceptions.kt index f99738f8e03c96..ea2f12b93ffb89 100644 --- a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemExceptions.kt +++ b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemExceptions.kt @@ -1,6 +1,6 @@ package expo.modules.filesystem -import expo.modules.interfaces.filesystem.Permission import expo.modules.kotlin.exception.CodedException +import expo.modules.kotlin.services.FilePermissionService internal class CopyOrMoveDirectoryToFileException : CodedException("Unable to copy or move a folder to a file") @@ -29,7 +29,7 @@ internal class UnableToCreateException(reason: String) : "Unable to create file or directory: $reason" ) -internal class InvalidPermissionException(permission: Permission) : +internal class InvalidPermissionException(permission: FilePermissionService.Permission) : CodedException( "Missing '${permission.name}' permission for accessing the file." ) diff --git a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemFile.kt b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemFile.kt index 8576656bea678e..208abbac7055ad 100644 --- a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemFile.kt +++ b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemFile.kt @@ -2,7 +2,7 @@ package expo.modules.filesystem import android.net.Uri import android.util.Base64 -import expo.modules.interfaces.filesystem.Permission +import expo.modules.kotlin.services.FilePermissionService import expo.modules.kotlin.typedarray.TypedArray import java.io.FileOutputStream import java.security.MessageDigest @@ -15,14 +15,14 @@ class FileSystemFile(uri: Uri) : FileSystemPath(uri) { // This makes sure that if a file already exists at a location, it is the correct type so that all available operations perform as expected. // After calling this function, we can use the `isDirectory` and `isFile` functions safely as they will match the shared class used. override fun validateType() { - validatePermission(Permission.READ) + validatePermission(FilePermissionService.Permission.READ) if (file.exists() && file.isDirectory()) { throw InvalidTypeFileException() } } val exists: Boolean get() { - return if (checkPermission(Permission.READ)) { + return if (checkPermission(FilePermissionService.Permission.READ)) { file.isFile() } else { false @@ -31,7 +31,7 @@ class FileSystemFile(uri: Uri) : FileSystemPath(uri) { fun create(options: CreateOptions = CreateOptions()) { validateType() - validatePermission(Permission.WRITE) + validatePermission(FilePermissionService.Permission.WRITE) validateCanCreate(options) if (uri.isContentUri) { throw UnableToCreateException("create function does not work with SAF Uris, use `createDirectory` and `createFile` instead") @@ -50,7 +50,7 @@ class FileSystemFile(uri: Uri) : FileSystemPath(uri) { fun write(content: String, append: Boolean = false) { validateType() - validatePermission(Permission.WRITE) + validatePermission(FilePermissionService.Permission.WRITE) if (!exists) { create() } @@ -61,7 +61,7 @@ class FileSystemFile(uri: Uri) : FileSystemPath(uri) { fun write(content: TypedArray, append: Boolean = false) { validateType() - validatePermission(Permission.WRITE) + validatePermission(FilePermissionService.Permission.WRITE) if (!exists) { create() } @@ -80,7 +80,7 @@ class FileSystemFile(uri: Uri) : FileSystemPath(uri) { fun write(content: ByteArray, append: Boolean = false) { validateType() - validatePermission(Permission.WRITE) + validatePermission(FilePermissionService.Permission.WRITE) if (!exists) { create() } @@ -102,7 +102,7 @@ class FileSystemFile(uri: Uri) : FileSystemPath(uri) { fun text(): String { validateType() - validatePermission(Permission.READ) + validatePermission(FilePermissionService.Permission.READ) return file.inputStream().use { inputStream -> inputStream.bufferedReader().use { it.readText() } } @@ -110,7 +110,7 @@ class FileSystemFile(uri: Uri) : FileSystemPath(uri) { fun base64(): String { validateType() - validatePermission(Permission.READ) + validatePermission(FilePermissionService.Permission.READ) file.inputStream().use { return Base64.encodeToString(it.readBytes(), Base64.NO_WRAP) } @@ -118,7 +118,7 @@ class FileSystemFile(uri: Uri) : FileSystemPath(uri) { fun bytes(): ByteArray { validateType() - validatePermission(Permission.READ) + validatePermission(FilePermissionService.Permission.READ) file.inputStream().use { return it.readBytes() } @@ -126,13 +126,13 @@ class FileSystemFile(uri: Uri) : FileSystemPath(uri) { fun asContentUri(): Uri { validateType() - validatePermission(Permission.READ) + validatePermission(FilePermissionService.Permission.READ) return file.getContentUri(appContext ?: throw MissingAppContextException()) } @OptIn(ExperimentalStdlibApi::class) val md5: String get() { - validatePermission(Permission.READ) + validatePermission(FilePermissionService.Permission.READ) val md = MessageDigest.getInstance("MD5") file.inputStream().use { val digest = md.digest(it.readBytes()) @@ -154,7 +154,7 @@ class FileSystemFile(uri: Uri) : FileSystemPath(uri) { fun info(options: InfoOptions?): FileInfo { validateType() - validatePermission(Permission.READ) + validatePermission(FilePermissionService.Permission.READ) if (!file.exists()) { val fileInfo = FileInfo( exists = false, diff --git a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt index 46239640890388..e4c0662650bebf 100644 --- a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt +++ b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt @@ -6,13 +6,13 @@ import android.os.Build import android.util.Base64 import android.webkit.URLUtil import androidx.annotation.RequiresApi -import expo.modules.interfaces.filesystem.Permission import expo.modules.kotlin.activityresult.AppContextActivityResultLauncher import expo.modules.kotlin.devtools.await import expo.modules.kotlin.exception.Exceptions import expo.modules.kotlin.functions.Coroutine import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition +import expo.modules.kotlin.services.FilePermissionService import expo.modules.kotlin.typedarray.TypedArray import expo.modules.kotlin.types.Either import okhttp3.OkHttpClient @@ -50,7 +50,7 @@ class FileSystemModule : Module() { } AsyncFunction("downloadFileAsync") Coroutine { url: URI, to: FileSystemPath, options: DownloadOptions? -> - to.validatePermission(Permission.WRITE) + to.validatePermission(FilePermissionService.Permission.WRITE) val requestBuilder = Request.Builder().url(url.toURL()) options?.headers?.forEach { (key, value) -> @@ -124,7 +124,7 @@ class FileSystemModule : Module() { appContext.reactContext ?: throw Exceptions.ReactContextLost(), file.path ) - if (permissions.contains(Permission.READ) && file.exists()) { + if (permissions.contains(FilePermissionService.Permission.READ) && file.exists()) { PathInfo(exists = file.exists(), isDirectory = file.isDirectory) } else { PathInfo(exists = false, isDirectory = null) diff --git a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemPath.kt b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemPath.kt index 49a04520c130ed..4628c2e17a1cdc 100644 --- a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemPath.kt +++ b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemPath.kt @@ -7,8 +7,8 @@ import expo.modules.filesystem.unifiedfile.AssetFile import expo.modules.filesystem.unifiedfile.JavaFile import expo.modules.filesystem.unifiedfile.SAFDocumentFile import expo.modules.filesystem.unifiedfile.UnifiedFileInterface -import expo.modules.interfaces.filesystem.Permission import expo.modules.kotlin.exception.Exceptions +import expo.modules.kotlin.services.FilePermissionService import expo.modules.kotlin.sharedobjects.SharedObject import java.io.File import java.util.EnumSet @@ -107,13 +107,13 @@ abstract class FileSystemPath(var uri: Uri) : SharedObject() { return destination.javaFile } - fun validatePermission(permission: Permission) { + fun validatePermission(permission: FilePermissionService.Permission) { if (!checkPermission(permission)) { throw InvalidPermissionException(permission) } } - fun checkPermission(permission: Permission): Boolean { + fun checkPermission(permission: FilePermissionService.Permission): Boolean { if (uri.isContentUri) { // TODO: Consider adding a check for content URIs (not in legacy FS) return true @@ -125,7 +125,7 @@ abstract class FileSystemPath(var uri: Uri) : SharedObject() { val permissions = appContext?.filePermission?.getPathPermissions( appContext?.reactContext ?: throw Exceptions.ReactContextLost(), javaFile.path - ) ?: EnumSet.noneOf(Permission::class.java) + ) ?: EnumSet.noneOf(FilePermissionService.Permission::class.java) return permissions.contains(permission) } @@ -138,8 +138,8 @@ abstract class FileSystemPath(var uri: Uri) : SharedObject() { fun copy(to: FileSystemPath) { validateType() to.validateType() - validatePermission(Permission.READ) - to.validatePermission(Permission.WRITE) + validatePermission(FilePermissionService.Permission.READ) + to.validatePermission(FilePermissionService.Permission.WRITE) javaFile.copyRecursively(getMoveOrCopyPath(to)) } @@ -147,8 +147,8 @@ abstract class FileSystemPath(var uri: Uri) : SharedObject() { fun move(to: FileSystemPath) { validateType() to.validateType() - validatePermission(Permission.WRITE) - to.validatePermission(Permission.WRITE) + validatePermission(FilePermissionService.Permission.WRITE) + to.validatePermission(FilePermissionService.Permission.WRITE) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val destination = getMoveOrCopyPath(to) @@ -163,7 +163,7 @@ abstract class FileSystemPath(var uri: Uri) : SharedObject() { fun rename(newName: String) { validateType() - validatePermission(Permission.WRITE) + validatePermission(FilePermissionService.Permission.WRITE) val newFile = File(javaFile.parent, newName) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { javaFile.toPath().moveTo(newFile.toPath()) diff --git a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/legacy/FileSystemLegacyModule.kt b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/legacy/FileSystemLegacyModule.kt index 40abfb1899efe8..92cffd0f05c296 100644 --- a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/legacy/FileSystemLegacyModule.kt +++ b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/legacy/FileSystemLegacyModule.kt @@ -15,11 +15,11 @@ import android.util.Log import androidx.core.content.FileProvider import androidx.documentfile.provider.DocumentFile import expo.modules.core.errors.ModuleDestroyedException -import expo.modules.interfaces.filesystem.Permission import expo.modules.kotlin.Promise import expo.modules.kotlin.exception.Exceptions import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition +import expo.modules.kotlin.services.FilePermissionService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -125,7 +125,7 @@ open class FileSystemLegacyModule : Module() { uriStr = parseFileUri(uriStr as String) absoluteUri = Uri.parse(uriStr) } - ensurePermission(absoluteUri, Permission.READ) + ensurePermission(absoluteUri, FilePermissionService.Permission.READ) if (uri.scheme == "file") { val file = absoluteUri.toFile() @@ -177,7 +177,7 @@ open class FileSystemLegacyModule : Module() { AsyncFunction("readAsStringAsync") { uriStr: String, options: ReadingOptions -> val uri = Uri.parse(slashifyFilePath(uriStr)) - ensurePermission(uri, Permission.READ) + ensurePermission(uri, FilePermissionService.Permission.READ) // TODO:Bacon: Add more encoding types to match iOS val encoding = options.encoding @@ -208,7 +208,7 @@ open class FileSystemLegacyModule : Module() { AsyncFunction("writeAsStringAsync") { uriStr: String, contents: String, options: WritingOptions -> val uri = Uri.parse(slashifyFilePath(uriStr)) - ensurePermission(uri, Permission.WRITE) + ensurePermission(uri, FilePermissionService.Permission.WRITE) val encoding = options.encoding val append = options.append getOutputStream(uri, append).use { out -> @@ -224,7 +224,7 @@ open class FileSystemLegacyModule : Module() { AsyncFunction("deleteAsync") { uriStr: String, options: DeletingOptions -> val uri = Uri.parse(slashifyFilePath(uriStr)) val appendedUri = Uri.withAppendedPath(uri, "..") - ensurePermission(appendedUri, Permission.WRITE, "Location '$uri' isn't deletable.") + ensurePermission(appendedUri, FilePermissionService.Permission.WRITE, "Location '$uri' isn't deletable.") if (uri.scheme == "file") { val file = uri.toFile() @@ -262,9 +262,13 @@ open class FileSystemLegacyModule : Module() { AsyncFunction("moveAsync") { options: RelocatingOptions -> val fromUri = Uri.parse(slashifyFilePath(options.from)) - ensurePermission(Uri.withAppendedPath(fromUri, ".."), Permission.WRITE, "Location '$fromUri' isn't movable.") + ensurePermission( + Uri.withAppendedPath(fromUri, ".."), + FilePermissionService.Permission.WRITE, + "Location '$fromUri' isn't movable." + ) val toUri = Uri.parse(slashifyFilePath(options.to)) - ensurePermission(toUri, Permission.WRITE) + ensurePermission(toUri, FilePermissionService.Permission.WRITE) if (fromUri.scheme == "file") { val from = fromUri.toFile() @@ -289,9 +293,9 @@ open class FileSystemLegacyModule : Module() { AsyncFunction("copyAsync") { options: RelocatingOptions -> val fromUri = Uri.parse(slashifyFilePath(options.from)) - ensurePermission(fromUri, Permission.READ, "Location '$fromUri' isn't readable.") + ensurePermission(fromUri, FilePermissionService.Permission.READ, "Location '$fromUri' isn't readable.") val toUri = Uri.parse(slashifyFilePath(options.to)) - ensurePermission(toUri, Permission.WRITE) + ensurePermission(toUri, FilePermissionService.Permission.WRITE) when { fromUri.scheme == "file" -> { @@ -350,7 +354,7 @@ open class FileSystemLegacyModule : Module() { AsyncFunction("makeDirectoryAsync") { uriStr: String, options: MakeDirectoryOptions -> val uri = Uri.parse(slashifyFilePath(uriStr)) - ensurePermission(uri, Permission.WRITE) + ensurePermission(uri, FilePermissionService.Permission.WRITE) if (uri.scheme == "file") { val file = uri.toFile() val previouslyCreated = file.isDirectory @@ -368,7 +372,7 @@ open class FileSystemLegacyModule : Module() { AsyncFunction("readDirectoryAsync") { uriStr: String? -> val uri = Uri.parse(slashifyFilePath(uriStr)) - ensurePermission(uri, Permission.READ) + ensurePermission(uri, FilePermissionService.Permission.READ) if (uri.scheme == "file") { val file = uri.toFile() @@ -403,8 +407,8 @@ open class FileSystemLegacyModule : Module() { AsyncFunction("getContentUriAsync") { uri: String -> val fileUri = Uri.parse(slashifyFilePath(uri)) - ensurePermission(fileUri, Permission.WRITE) - ensurePermission(fileUri, Permission.READ) + ensurePermission(fileUri, FilePermissionService.Permission.WRITE) + ensurePermission(fileUri, FilePermissionService.Permission.READ) fileUri.checkIfFileDirExists() if (fileUri.scheme == "file") { @@ -417,7 +421,7 @@ open class FileSystemLegacyModule : Module() { AsyncFunction("readSAFDirectoryAsync") { uriStr: String -> val uri = Uri.parse(slashifyFilePath(uriStr)) - ensurePermission(uri, Permission.READ) + ensurePermission(uri, FilePermissionService.Permission.READ) if (uri.isSAFUri) { val file = DocumentFile.fromTreeUri(context, uri) @@ -433,7 +437,7 @@ open class FileSystemLegacyModule : Module() { AsyncFunction("makeSAFDirectoryAsync") { uriStr: String, dirName: String -> val uri = Uri.parse(slashifyFilePath(uriStr)) - ensurePermission(uri, Permission.WRITE) + ensurePermission(uri, FilePermissionService.Permission.WRITE) if (!uri.isSAFUri) { throw IOException("The URI '$uri' is not a Storage Access Framework URI. Try using FileSystem.makeDirectoryAsync instead.") @@ -452,7 +456,7 @@ open class FileSystemLegacyModule : Module() { AsyncFunction("createSAFFileAsync") { uriStr: String, fileName: String, mimeType: String -> val uri = Uri.parse(slashifyFilePath(uriStr)) - ensurePermission(uri, Permission.WRITE) + ensurePermission(uri, FilePermissionService.Permission.WRITE) if (uri.isSAFUri) { val dir = getNearestSAFFile(uri) if (dir == null || !dir.isDirectory) { @@ -565,7 +569,7 @@ open class FileSystemLegacyModule : Module() { AsyncFunction("downloadAsync") { url: String, uriStr: String?, options: DownloadOptionsLegacy, promise: Promise -> val uri = Uri.parse(slashifyFilePath(uriStr)) - ensurePermission(uri, Permission.WRITE) + ensurePermission(uri, FilePermissionService.Permission.WRITE) uri.checkIfFileDirExists() when { @@ -767,7 +771,7 @@ open class FileSystemLegacyModule : Module() { } } - private fun permissionsForPath(path: String?): EnumSet? { + private fun permissionsForPath(path: String?): EnumSet? { if (path == null) { return null } @@ -776,22 +780,22 @@ open class FileSystemLegacyModule : Module() { private fun permissionsForUri(uri: Uri) = when { uri.isSAFUri -> permissionsForSAFUri(uri) - uri.scheme == "content" -> EnumSet.of(Permission.READ) - uri.scheme == "asset" -> EnumSet.of(Permission.READ) + uri.scheme == "content" -> EnumSet.of(FilePermissionService.Permission.READ) + uri.scheme == "asset" -> EnumSet.of(FilePermissionService.Permission.READ) uri.scheme == "file" -> permissionsForPath(uri.path) - uri.scheme == null -> EnumSet.of(Permission.READ) - else -> EnumSet.noneOf(Permission::class.java) + uri.scheme == null -> EnumSet.of(FilePermissionService.Permission.READ) + else -> EnumSet.noneOf(FilePermissionService.Permission::class.java) } - private fun permissionsForSAFUri(uri: Uri): EnumSet { + private fun permissionsForSAFUri(uri: Uri): EnumSet { val documentFile = getNearestSAFFile(uri) - return EnumSet.noneOf(Permission::class.java).apply { + return EnumSet.noneOf(FilePermissionService.Permission::class.java).apply { if (documentFile != null) { if (documentFile.canRead()) { - add(Permission.READ) + add(FilePermissionService.Permission.READ) } if (documentFile.canWrite()) { - add(Permission.WRITE) + add(FilePermissionService.Permission.WRITE) } } } @@ -800,18 +804,18 @@ open class FileSystemLegacyModule : Module() { // For now we only need to ensure one permission at a time, this allows easier error message strings, // we can generalize this when needed later @Throws(IOException::class) - private fun ensurePermission(uri: Uri, permission: Permission, errorMsg: String) { + private fun ensurePermission(uri: Uri, permission: FilePermissionService.Permission, errorMsg: String) { if (permissionsForUri(uri)?.contains(permission) != true) { throw IOException(errorMsg) } } @Throws(IOException::class) - private fun ensurePermission(uri: Uri, permission: Permission) { - if (permission == Permission.READ) { + private fun ensurePermission(uri: Uri, permission: FilePermissionService.Permission) { + if (permission == FilePermissionService.Permission.READ) { ensurePermission(uri, permission, "Location '$uri' isn't readable.") } - if (permission == Permission.WRITE) { + if (permission == FilePermissionService.Permission.WRITE) { ensurePermission(uri, permission, "Location '$uri' isn't writable.") } ensurePermission(uri, permission, "Location '$uri' doesn't have permission '${permission.name}'.") @@ -888,7 +892,7 @@ open class FileSystemLegacyModule : Module() { @Throws(IOException::class) private fun createUploadRequest(url: String, fileUriString: String, options: FileSystemUploadOptions, decorator: RequestBodyDecorator): Request { val fileUri = Uri.parse(slashifyFilePath(fileUriString)) - ensurePermission(fileUri, Permission.READ) + ensurePermission(fileUri, FilePermissionService.Permission.READ) fileUri.checkIfFileExists() val requestBuilder = Request.Builder().url(url) diff --git a/packages/expo-modules-core/android/src/main/java/expo/modules/interfaces/filesystem/Permission.java b/packages/expo-modules-core/android/src/main/java/expo/modules/interfaces/filesystem/Permission.java deleted file mode 100644 index 782f890b9fda22..00000000000000 --- a/packages/expo-modules-core/android/src/main/java/expo/modules/interfaces/filesystem/Permission.java +++ /dev/null @@ -1,5 +0,0 @@ -package expo.modules.interfaces.filesystem; - -public enum Permission { - READ, WRITE, -} diff --git a/packages/expo-modules-core/android/src/main/java/expo/modules/interfaces/filesystem/Permission.kt b/packages/expo-modules-core/android/src/main/java/expo/modules/interfaces/filesystem/Permission.kt new file mode 100644 index 00000000000000..d579d1c5c9d1e2 --- /dev/null +++ b/packages/expo-modules-core/android/src/main/java/expo/modules/interfaces/filesystem/Permission.kt @@ -0,0 +1,6 @@ +package expo.modules.interfaces.filesystem + +import expo.modules.kotlin.services.FilePermissionService + +@Deprecated("Use FilePermissionService.Permission instead", ReplaceWith("FilePermissionService.Permission")) +typealias Permission = FilePermissionService.Permission diff --git a/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/services/FilePermissionService.kt b/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/services/FilePermissionService.kt index fb7c7a584c3e46..69149e9bedaf24 100644 --- a/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/services/FilePermissionService.kt +++ b/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/services/FilePermissionService.kt @@ -1,13 +1,17 @@ package expo.modules.kotlin.services import android.content.Context -import expo.modules.interfaces.filesystem.Permission import java.io.File import java.io.IOException import java.util.EnumSet // The class needs to be 'open', because it's inherited in expoview open class FilePermissionService : Service { + enum class Permission { + READ, + WRITE + } + open fun getPathPermissions(context: Context, path: String): EnumSet = getInternalPathPermissions(path, context) ?: getExternalPathPermissions(path) diff --git a/packages/expo-sharing/android/src/main/java/expo/modules/sharing/SharingModule.kt b/packages/expo-sharing/android/src/main/java/expo/modules/sharing/SharingModule.kt index 29a1378574e18b..69ccba68e28af7 100644 --- a/packages/expo-sharing/android/src/main/java/expo/modules/sharing/SharingModule.kt +++ b/packages/expo-sharing/android/src/main/java/expo/modules/sharing/SharingModule.kt @@ -6,11 +6,11 @@ import android.content.pm.PackageManager import android.net.Uri import androidx.core.content.FileProvider import expo.modules.core.errors.InvalidArgumentException -import expo.modules.interfaces.filesystem.Permission import expo.modules.kotlin.Promise import expo.modules.kotlin.exception.Exceptions import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition +import expo.modules.kotlin.services.FilePermissionService import java.io.File import java.net.URLConnection @@ -110,7 +110,7 @@ class SharingModule : Module() { return permissions .getPathPermissions(context, url) - .contains(Permission.READ) + .contains(FilePermissionService.Permission.READ) } private fun createSharingIntent(uri: Uri, mimeType: String?) = diff --git a/packages/expo-video-thumbnails/android/src/main/java/expo/modules/videothumbnails/VideoThumbnailsModule.kt b/packages/expo-video-thumbnails/android/src/main/java/expo/modules/videothumbnails/VideoThumbnailsModule.kt index 9e8715607c62cf..2c2d79428ff8b7 100644 --- a/packages/expo-video-thumbnails/android/src/main/java/expo/modules/videothumbnails/VideoThumbnailsModule.kt +++ b/packages/expo-video-thumbnails/android/src/main/java/expo/modules/videothumbnails/VideoThumbnailsModule.kt @@ -8,12 +8,12 @@ import android.util.Log import android.webkit.URLUtil import expo.modules.core.errors.ModuleDestroyedException import expo.modules.core.utilities.FileUtilities -import expo.modules.interfaces.filesystem.Permission import expo.modules.kotlin.Promise import expo.modules.kotlin.exception.CodedException import expo.modules.kotlin.exception.Exceptions import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition +import expo.modules.kotlin.services.FilePermissionService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -115,7 +115,8 @@ class VideoThumbnailsModule : Module() { private fun isAllowedToRead(url: String): Boolean { val permissionModuleInterface = appContext.filePermission ?: throw FilePermissionsModuleNotFound() - return permissionModuleInterface.getPathPermissions(context, url).contains(Permission.READ) + return permissionModuleInterface.getPathPermissions(context, url) + .contains(FilePermissionService.Permission.READ) } private inline fun withModuleScope(promise: Promise, crossinline block: () -> Unit) = moduleCoroutineScope.launch { From 0529b55fade87d6d8f5287fe2bb19813e8d27407 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 26 Feb 2026 11:33:53 +0000 Subject: [PATCH 07/19] chore: Remove `package.json` resolutions for `@react-navigation/*` packages (#43458) # Why We seem to be aligned on all versions and the pins here are redundant (and shouldn't ever do anything). They used to be necessary because of transitive dependencies and templates being misaligned on purpose, but those aren't misaligned anymore. The main motivation is to be able to catch duplicate issues more easily. The lockfile changes are an easy indicator to spot misaligned versions. Although the templates aren't actively installed in the monorepo, this gives us an extra opportunity to spot issues, if we do have checks and tests that install them in the monorepo in the future. The other motivation is to reduce the maintenance burden of performing `@react-navigation/*` upgrades, since this will then flag duplicates in the lockfile locally while upgrading. # How - Remove `@react-navigation/*` pins # Test Plan - Check lockfile; noop changes # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- package.json | 8 -------- yarn.lock | 22 +++++++++++----------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 618f7b9ced391f..aeee41787437b8 100644 --- a/package.json +++ b/package.json @@ -27,14 +27,6 @@ "@types/babel__generator": "^7.27.0", "@types/babel__template": "^7.4.4", "@types/babel__traverse": "^7.20.7", - "@react-navigation/bottom-tabs": "7.10.1", - "@react-navigation/core": "7.14.0", - "@react-navigation/drawer": "7.7.2", - "@react-navigation/elements": "2.9.5", - "@react-navigation/native": "7.1.28", - "@react-navigation/native-stack": "7.10.1", - "@react-navigation/routers": "7.5.2", - "@react-navigation/stack": "7.6.7", "**/util": "~0.12.4" }, "dependencies": { diff --git a/yarn.lock b/yarn.lock index f414936cb665ab..9df8cbbfc08d8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3771,7 +3771,7 @@ invariant "^2.2.4" nullthrows "^1.1.1" -"@react-navigation/bottom-tabs@7.10.1", "@react-navigation/bottom-tabs@^7.10.1", "@react-navigation/bottom-tabs@^7.3.10", "@react-navigation/bottom-tabs@^7.7.3": +"@react-navigation/bottom-tabs@^7.10.1", "@react-navigation/bottom-tabs@^7.3.10", "@react-navigation/bottom-tabs@^7.7.3": version "7.10.1" resolved "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.10.1.tgz#8d28e623f1f503c0e66b55837fa03a78a20ea2a8" integrity sha512-MirOzKEe/rRwPSE9HMrS4niIo0LyUhewlvd01TpzQ1ipuXjH2wJbzAM9gS/r62zriB6HMHz2OY6oIRduwQJtTw== @@ -3780,7 +3780,7 @@ color "^4.2.3" sf-symbols-typescript "^2.1.0" -"@react-navigation/core@7.14.0", "@react-navigation/core@^7.14.0": +"@react-navigation/core@^7.14.0": version "7.14.0" resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.14.0.tgz#d24f93d424ab33f645262dc4775e4708aa3d9a8b" integrity sha512-tMpzskBzVp0E7CRNdNtJIdXjk54Kwe/TF9ViXAef+YFM1kSfGv4e/B2ozfXE+YyYgmh4WavTv8fkdJz1CNyu+g== @@ -3794,7 +3794,7 @@ use-latest-callback "^0.2.4" use-sync-external-store "^1.5.0" -"@react-navigation/drawer@7.7.2", "@react-navigation/drawer@^7.7.2": +"@react-navigation/drawer@^7.7.2": version "7.7.2" resolved "https://registry.yarnpkg.com/@react-navigation/drawer/-/drawer-7.7.2.tgz#86f0fcf7065d8754d7532bebd4800a8fa17afde2" integrity sha512-393VoJLiQe1wMw6FxP+ajCgJRRI3PuEX+OA1YQG1my1BB2AbrbBld6e5uyvU5eoYykIpkQ5D1mxDmZ5MhF4yPA== @@ -3804,7 +3804,7 @@ react-native-drawer-layout "^4.2.0" use-latest-callback "^0.2.4" -"@react-navigation/elements@2.9.5", "@react-navigation/elements@^2.8.1", "@react-navigation/elements@^2.8.3", "@react-navigation/elements@^2.9.5": +"@react-navigation/elements@^2.8.1", "@react-navigation/elements@^2.8.3", "@react-navigation/elements@^2.9.5": version "2.9.5" resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.9.5.tgz#29f68c4975351724dcfe1d3bdc76c4d6dc65fc33" integrity sha512-iHZU8rRN1014Upz73AqNVXDvSMZDh5/ktQ1CMe21rdgnOY79RWtHHBp9qOS3VtqlUVYGkuX5GEw5mDt4tKdl0g== @@ -3813,7 +3813,7 @@ use-latest-callback "^0.2.4" use-sync-external-store "^1.5.0" -"@react-navigation/native-stack@7.10.1", "@react-navigation/native-stack@^7.10.1": +"@react-navigation/native-stack@^7.10.1": version "7.10.1" resolved "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.10.1.tgz#de6551286a5e9e2b0c466daa83e0f9bf0321815b" integrity sha512-8jt7olKysn07HuKKSjT/ahZZTV+WaZa96o9RI7gAwh7ATlUDY02rIRttwvCyjovhSjD9KCiuJ+Hd4kwLidHwJw== @@ -3823,7 +3823,7 @@ sf-symbols-typescript "^2.1.0" warn-once "^0.1.1" -"@react-navigation/native@7.1.28", "@react-navigation/native@^7.1.28", "@react-navigation/native@^7.1.6": +"@react-navigation/native@^7.1.28", "@react-navigation/native@^7.1.6": version "7.1.28" resolved "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.28.tgz#1ee75cf3a8b3e4365f94c5d657bb3c015e387720" integrity sha512-d1QDn+KNHfHGt3UIwOZvupvdsDdiHYZBEj7+wL2yDVo3tMezamYy60H9s3EnNVE1Ae1ty0trc7F2OKqo/RmsdQ== @@ -3834,14 +3834,14 @@ nanoid "^3.3.11" use-latest-callback "^0.2.4" -"@react-navigation/routers@7.5.2", "@react-navigation/routers@^7.5.3": - version "7.5.2" - resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-7.5.2.tgz#c885a66a76286f1c4c94261814ceddad628fbbea" - integrity sha512-kymreY5aeTz843E+iPAukrsOtc7nabAH6novtAPREmmGu77dQpfxPB2ZWpKb5nRErIRowp1kYRoN2Ckl+S6JYw== +"@react-navigation/routers@^7.5.3": + version "7.5.3" + resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-7.5.3.tgz#8002930ef5f62351be2475d0dffde3ffaee174d7" + integrity sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg== dependencies: nanoid "^3.3.11" -"@react-navigation/stack@7.6.7", "@react-navigation/stack@^7.6.7": +"@react-navigation/stack@^7.6.7": version "7.6.7" resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-7.6.7.tgz#8cd599681964ba930a15b70134bcbe38bf96424d" integrity sha512-8NZWKTBYRVl8oSvhLKs26C6Dw5a3OhyfRc8ITS9A0kRSYaaX/KcZpObbAxp8kCJfTaJ7ZmghyX2NCGwnKw6V7A== From c1c48a34a4c1e6ecee7d05a2922d21fd106c4a1c Mon Sep 17 00:00:00 2001 From: Gabriel Donadel Dall'Agnol Date: Thu, 26 Feb 2026 08:40:15 -0300 Subject: [PATCH 08/19] [gh] Migrate Test Suite Brownfield workflow to EAS (#43407) # Why Brownfield CI is taking a really long time to run on GitHub Action, and people are getting into long macOS runner queues. To avoid this, we should migrate the existing brownfield workflows to EAS # How Results: | Platform | GitHub Actions | EAS | Improvement | |----------|----------------|-----|------------| | Android | 16m 8s (Debug) + 17m 3s (Release) = **33m 11s** | **10m 50s** (Debug + Release) | ~22m 21s faster | | iOS | 48m 33s (Debug) + 41m 21s (Release) = **89m 54s** | **10m 15s** (Debug + Release) | ~79m 39s faster | # Test Plan CI should be green # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- .github/workflows/test-suite-brownfield.yml | 153 ---------- .../.eas/workflows/test-suite-brownfield.yml | 287 ++++++++++++++++++ 2 files changed, 287 insertions(+), 153 deletions(-) delete mode 100644 .github/workflows/test-suite-brownfield.yml create mode 100644 apps/expo-workflow-testing/.eas/workflows/test-suite-brownfield.yml diff --git a/.github/workflows/test-suite-brownfield.yml b/.github/workflows/test-suite-brownfield.yml deleted file mode 100644 index 18aac6d473e931..00000000000000 --- a/.github/workflows/test-suite-brownfield.yml +++ /dev/null @@ -1,153 +0,0 @@ -name: Test Suite Brownfield - -on: - workflow_dispatch: {} - pull_request: - paths: - - .github/workflows/test-suite-brownfield.yml - - apps/brownfield-tester/** - - packages/expo/** - - packages/expo-modules-core/** - - packages/expo-dev-client/** - - packages/expo-updates/** - - '!**.md' - - '!**/__tests__/**' - - '!**/__mocks__/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - detect-platform-for-e2e: - runs-on: ubuntu-24.04 - outputs: - should_run_ios: ${{ steps.changes.outputs.should_run_ios }} - should_run_android: ${{ steps.changes.outputs.should_run_android }} - steps: - - name: šŸ‘€ Checkout - uses: actions/checkout@v5 - - name: 🧐 Detect platform change - id: changes - uses: ./.github/actions/detect-platform-change - with: - android-paths: >- - "apps/brownfield-tester/**/android/**", - "packages/{expo,expo-modules-core,expo-dev-client,expo-updates}/**/android/**" - ios-paths: >- - "apps/brownfield-tester/**/ios/**", - "packages/{expo,expo-modules-core,expo-dev-client,expo-updates}/**/ios/**" - common-paths: >- - .github/workflows/test-suite-brownfield.yml, - "apps/brownfield-tester/**", - "!apps/brownfield-tester/**/{ios,android}/**", - "packages/{expo,expo-modules-core,expo-dev-client,expo-updates}/**", - "!packages/{expo,expo-modules-core,expo-dev-client,expo-updates}/**/{ios,android}/**" - ios-build: - needs: detect-platform-for-e2e - if: needs.detect-platform-for-e2e.outputs.should_run_ios == 'true' - strategy: - fail-fast: true - matrix: - build-type: [debug, release] - runs-on: macos-15 - steps: - - name: šŸ‘€ Checkout - uses: actions/checkout@v4 - with: - submodules: true - - name: šŸ”Ø Switch to Xcode 26.2 - run: sudo xcode-select --switch /Applications/Xcode_26.2.app - - name: āž• Add `bin` to GITHUB_PATH - run: echo "$(pwd)/bin" >> $GITHUB_PATH - - name: šŸ’Ž Setup Ruby and install gems - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - ruby-version: 3.2.2 - - name: ā™»ļø Restore caches - uses: ./.github/actions/expo-caches - id: expo-caches - with: - yarn-workspace: 'true' - yarn-tools: 'true' - - name: 🧶 Install node modules in root dir - run: yarn install --frozen-lockfile - - name: šŸ Build iOS Project - working-directory: ./apps/brownfield-tester - run: | - pod install --project-directory=ios - xcodebuild -workspace ios/BrownfieldTester.xcworkspace -scheme BrownfieldTester -configuration $CONFIGURATION -sdk iphonesimulator -derivedDataPath "ios/build" | xcpretty - shell: bash - env: - NODE_ENV: production - CONFIGURATION: ${{ matrix.build-type == 'release' && 'Release' || 'Debug' }} - - name: šŸ“ø Upload builds - uses: actions/upload-artifact@v4 - if: ${{ github.event_name == 'workflow_dispatch' && matrix.build-type == 'release' }} # Only archive release builds - with: - name: ios-builds-${{ matrix.build-type }} - path: ${{ runner.temp }}/brownfield-tester/ios/build/**/BrownfieldTester.app/ - - name: šŸ”” Notify on Slack - uses: ./.github/actions/slack-notify - if: failure() && (github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) - with: - webhook: ${{ secrets.slack_webhook_ios }} - channel: '#expo-ios' - author_name: Brownfield Test Suite (iOS) - android-build: - needs: detect-platform-for-e2e - if: needs.detect-platform-for-e2e.outputs.should_run_android == 'true' - strategy: - fail-fast: true - matrix: - build-type: [debug, release] - runs-on: ubuntu-24.04 - env: - ORG_GRADLE_PROJECT_reactNativeArchitectures: x86_64 - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4096m -XX:MaxMetaspaceSize=4096m" - steps: - - name: šŸ‘€ Checkout - uses: actions/checkout@v5 - - name: 🧹 Cleanup GitHub Linux runner disk space - uses: ./.github/actions/cleanup-linux-disk-space - - name: šŸš€ Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: šŸ”Ø Use JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - name: ā™»ļø Restore caches - uses: ./.github/actions/expo-caches - id: expo-caches - with: - gradle: 'true' - yarn-workspace: 'true' - yarn-tools: 'true' - react-native-gradle-downloads: 'true' - - name: 🧶 Install node modules in root dir - run: yarn install --frozen-lockfile - - name: šŸ¤– Build Android project - run: | - cd android && ./gradlew ":app:assemble$VARIANT" - shell: bash - working-directory: ./apps/brownfield-tester - env: - NODE_ENV: production - VARIANT: ${{ matrix.build-type == 'release' && 'Release' || 'Debug' }} - - name: šŸ“ø Upload builds - uses: actions/upload-artifact@v4 - if: ${{ github.event_name == 'workflow_dispatch' && matrix.build-type == 'release' }} # Only archive release builds - with: - name: android-builds-${{ matrix.build-type }} - path: ./apps/brownfield-tester/android/app/build/outputs/apk/ - - name: šŸ”” Notify on Slack - uses: ./.github/actions/slack-notify - if: failure() && (github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) - with: - webhook: ${{ secrets.slack_webhook_android }} - channel: '#expo-android' - author_name: Brownfield Test Suite (Android) diff --git a/apps/expo-workflow-testing/.eas/workflows/test-suite-brownfield.yml b/apps/expo-workflow-testing/.eas/workflows/test-suite-brownfield.yml new file mode 100644 index 00000000000000..ac8a48d83d6d87 --- /dev/null +++ b/apps/expo-workflow-testing/.eas/workflows/test-suite-brownfield.yml @@ -0,0 +1,287 @@ +name: Test Suite Brownfield Integrated + +on: + workflow_dispatch: {} + pull_request: + paths: + - apps/expo-workflow-testing/.eas/workflows/test-suite-brownfield.yml + - apps/brownfield-tester/** + - packages/expo/** + - packages/expo-modules-core/** + - packages/expo-dev-client/** + - packages/expo-updates/** + - '!**/*.md' + - '!**/__tests__/**' + - '!**/__mocks__/**' + +concurrency: + group: ${{ workflow.filename }}-${{ github.ref }} + cancel_in_progress: true + +defaults: + tools: + node: 22.14.0 + yarn: 1.22.22 + +jobs: + detect_platform: + name: Detect platform changes + runs_on: linux-medium + image: latest + outputs: + should_run_ios: ${{ steps.detect.outputs.should_run_ios }} + should_run_android: ${{ steps.detect.outputs.should_run_android }} + steps: + - id: detect + outputs: [should_run_ios, should_run_android] + run: | + EVENT_NAME="${{ github.event_name }}" + PR_NUMBER="${{ github.event.pull_request.number }}" + REPO="${{ github.event.repository.full_name }}" + echo "EVENT_NAME=$EVENT_NAME" + echo "PR_NUMBER=$PR_NUMBER" + echo "REPO=$REPO" + if [ "$EVENT_NAME" != "pull_request" ]; then + echo "Not a pull request — running both platforms" + set-output should_run_ios "true" + set-output should_run_android "true" + exit 0 + fi + + # Use GitHub API to get changed files — no git history needed + API_URL="https://api.github.com/repos/$REPO/pulls/$PR_NUMBER/files?per_page=100" + echo "Fetching changed files: $API_URL" + PAGE=1 + CHANGED_FILES="" + while true; do + RESPONSE=$(curl -sf "${API_URL}&page=$PAGE" || echo "") + if [ -z "$RESPONSE" ] || [ "$RESPONSE" = "[]" ]; then + break + fi + FILES=$(echo "$RESPONSE" | jq -r '.[].filename' 2>/dev/null || echo "") + if [ -z "$FILES" ]; then + break + fi + CHANGED_FILES="${CHANGED_FILES}${FILES}"$'\n' + PAGE=$((PAGE + 1)) + done + + echo "All changed files:" + echo "$CHANGED_FILES" + + # Filter out ignored files (markdown, license, config, etc.) + FILTERED=$(echo "$CHANGED_FILES" \ + | grep -v -E '\.(md)$' \ + | grep -v -E '(LICENSE|CHANGELOG|\.gitignore|\.npmignore|\.eslintrc|\.prettierrc|\.gitattributes|\.watchmanconfig|\.fingerprintignore)' \ + | grep -v -E '\.web\.' \ + || true) + + # Only keep files in relevant paths + RELEVANT=$(echo "$FILTERED" \ + | grep -E '^(apps/brownfield-tester/|packages/(expo|expo-modules-core|expo-dev-client|expo-updates)/|apps/expo-workflow-testing/\.eas/workflows/test-suite-brownfield\.yml)' \ + || true) + + if [ -z "$RELEVANT" ]; then + echo "No relevant file changes detected" + set-output should_run_ios "false" + set-output should_run_android "false" + exit 0 + fi + + echo "Relevant changes:" + echo "$RELEVANT" + + # Platform-specific: files under ios/ or android/ dirs + IOS_CHANGES=$(echo "$RELEVANT" | grep -E '/ios/' || true) + ANDROID_CHANGES=$(echo "$RELEVANT" | grep -E '/android/' || true) + # Common: relevant files NOT under ios/ or android/ + COMMON_CHANGES=$(echo "$RELEVANT" | grep -v -E '/(ios|android)/' || true) + + SHOULD_RUN_IOS="false" + SHOULD_RUN_ANDROID="false" + [ -n "$IOS_CHANGES" ] || [ -n "$COMMON_CHANGES" ] && SHOULD_RUN_IOS="true" + [ -n "$ANDROID_CHANGES" ] || [ -n "$COMMON_CHANGES" ] && SHOULD_RUN_ANDROID="true" + + echo "" + echo "should_run_ios=$SHOULD_RUN_IOS" + echo "should_run_android=$SHOULD_RUN_ANDROID" + set-output should_run_ios "$SHOULD_RUN_IOS" + set-output should_run_android "$SHOULD_RUN_ANDROID" + + ios: + name: iOS Build + needs: [detect_platform] + if: ${{ needs.detect_platform.outputs.should_run_ios == 'true' }} + runs_on: macos-large + image: latest + steps: + - uses: eas/checkout + - uses: eas/use_npm_token + - uses: eas/install_node_modules + - name: Install CocoaPods + working_directory: ../brownfield-tester + run: pod install --project-directory=ios + - name: Build iOS (Debug) + working_directory: ../brownfield-tester + env: + NODE_ENV: production + run: | + xcodebuild -workspace ios/BrownfieldTester.xcworkspace -scheme BrownfieldTester -configuration Debug -sdk iphonesimulator -derivedDataPath "ios/build" | xcpretty + - name: Build iOS (Release) + working_directory: ../brownfield-tester + env: + NODE_ENV: production + run: | + xcodebuild -workspace ios/BrownfieldTester.xcworkspace -scheme BrownfieldTester -configuration Release -sdk iphonesimulator -derivedDataPath "ios/build" | xcpretty + + android: + name: Android Build + needs: [detect_platform] + if: ${{ needs.detect_platform.outputs.should_run_android == 'true' }} + runs_on: linux-large + image: latest + env: + ORG_GRADLE_PROJECT_reactNativeArchitectures: x86_64 + GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx4096m -XX:MaxMetaspaceSize=4096m"' + steps: + - uses: eas/checkout + - uses: eas/use_npm_token + - uses: eas/install_node_modules + - name: Build Android (Debug) + working_directory: ../brownfield-tester/android + env: + NODE_ENV: production + run: ./gradlew :app:assembleDebug + - name: Build Android (Release) + working_directory: ../brownfield-tester/android + env: + NODE_ENV: production + run: ./gradlew :app:assembleRelease + + # Slack webhook URLs must be set as EAS environment variables (SLACK_WEBHOOK_IOS, SLACK_WEBHOOK_ANDROID) + # see https://github.com/expo/expo/pull/40224/commits/e5295e8518d07e9b16ad3ed23c46977232b6bb90 + notify_ios_failure: + if: ${{ failure() && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/sdk-')) }} + name: Notify iOS failure on Slack + after: [ios] + steps: + - run: | + COMMIT_SHA="${{ github.sha }}" + COMMIT_MESSAGE="${{ github.commit_message }}" + BRANCH="${{ github.ref_name }}" + WF_ID="${{ workflow.id }}" + ACTOR="${{ github.triggering_actor }}" + + if [ -z "$COMMIT_MESSAGE" ] || [ "$COMMIT_MESSAGE" = "undefined" ]; then + COMMIT_DISPLAY="${COMMIT_SHA:0:7}" + else + COMMIT_DISPLAY=$(echo "$COMMIT_MESSAGE" | head -n1) + fi + + PAYLOAD=$(jq -n \ + --arg text "🚨 Brownfield Test Suite (iOS) failed" \ + --arg wf_id "$WF_ID" \ + --arg branch "$BRANCH" \ + --arg commit_sha "$COMMIT_SHA" \ + --arg commit_display "$COMMIT_DISPLAY" \ + --arg actor "$ACTOR" \ + '{ + text: $text, + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "*Brownfield Test Suite (iOS) failed*" + } + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: ("*Workflow:*\n") + }, + { + type: "mrkdwn", + text: ("*Branch:*\n") + }, + { + type: "mrkdwn", + text: ("*Commit:*\n") + }, + { + type: "mrkdwn", + text: ("*Triggered by:*\n" + $actor) + } + ] + } + ] + }') + + curl -X POST "$SLACK_WEBHOOK_IOS" \ + -H 'Content-Type: application/json' \ + -d "$PAYLOAD" + + notify_android_failure: + if: ${{ failure() && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/sdk-')) }} + name: Notify Android failure on Slack + after: [android] + steps: + - run: | + COMMIT_SHA="${{ github.sha }}" + COMMIT_MESSAGE="${{ github.commit_message }}" + BRANCH="${{ github.ref_name }}" + WF_ID="${{ workflow.id }}" + ACTOR="${{ github.triggering_actor }}" + + if [ -z "$COMMIT_MESSAGE" ] || [ "$COMMIT_MESSAGE" = "undefined" ]; then + COMMIT_DISPLAY="${COMMIT_SHA:0:7}" + else + COMMIT_DISPLAY=$(echo "$COMMIT_MESSAGE" | head -n1) + fi + + PAYLOAD=$(jq -n \ + --arg text "🚨 Brownfield Test Suite (Android) failed" \ + --arg wf_id "$WF_ID" \ + --arg branch "$BRANCH" \ + --arg commit_sha "$COMMIT_SHA" \ + --arg commit_display "$COMMIT_DISPLAY" \ + --arg actor "$ACTOR" \ + '{ + text: $text, + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "*Brownfield Test Suite (Android) failed*" + } + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: ("*Workflow:*\n") + }, + { + type: "mrkdwn", + text: ("*Branch:*\n") + }, + { + type: "mrkdwn", + text: ("*Commit:*\n") + }, + { + type: "mrkdwn", + text: ("*Triggered by:*\n" + $actor) + } + ] + } + ] + }') + + curl -X POST "$SLACK_WEBHOOK_ANDROID" \ + -H 'Content-Type: application/json' \ + -d "$PAYLOAD" From 3f363efcebc63dcb62d06a5a595c125536175960 Mon Sep 17 00:00:00 2001 From: Aman Mittal Date: Thu, 26 Feb 2026 17:30:35 +0530 Subject: [PATCH 09/19] [docs] Add site-wide WebSite, Organization, Breadcrumb JSON-LD schema (#43363) --- docs/common/routes.test.ts | 175 +++++++++++++++++++- docs/common/routes.ts | 71 ++++++++ docs/components/DocumentationPage.tsx | 7 +- docs/constants/structured-data.test.ts | 68 ++++++++ docs/constants/structured-data.ts | 44 +++++ docs/pages/_app.tsx | 3 + docs/public/static/images/expo-logo.svg | 3 + docs/types/common.ts | 1 + docs/ui/components/StructuredData/index.tsx | 2 +- 9 files changed, 371 insertions(+), 3 deletions(-) create mode 100644 docs/constants/structured-data.test.ts create mode 100644 docs/constants/structured-data.ts create mode 100644 docs/public/static/images/expo-logo.svg diff --git a/docs/common/routes.test.ts b/docs/common/routes.test.ts index 567ce454f166e6..1e1c02655c3a88 100644 --- a/docs/common/routes.test.ts +++ b/docs/common/routes.test.ts @@ -1,4 +1,6 @@ -import { isReferencePath } from '~/common/routes'; +import type { NavigationRoute } from '~/types/common'; + +import { getBreadcrumbTrail, isReferencePath } from './routes'; describe(isReferencePath, () => { it('returns true for unversioned pathname', () => { @@ -17,3 +19,174 @@ describe(isReferencePath, () => { expect(isReferencePath('/build-reference/how-tos/')).toBe(false); }); }); + +describe(getBreadcrumbTrail, () => { + const mockRoutes: NavigationRoute[] = [ + { + type: 'section', + name: 'Develop', + href: '', + children: [ + { type: 'page', name: 'Tools', href: '/develop/tools' }, + { + type: 'group', + name: 'User interface', + href: '', + children: [ + { type: 'page', name: 'Fonts', href: '/develop/user-interface/fonts' }, + { type: 'page', name: 'Assets', href: '/develop/user-interface/assets' }, + ], + }, + ], + }, + { + type: 'section', + name: 'Deploy', + href: '', + children: [{ type: 'page', name: 'Build project', href: '/deploy/build-project' }], + }, + ]; + + it('returns empty array for pathname not in routes', () => { + expect(getBreadcrumbTrail(mockRoutes, '/nonexistent')).toEqual([]); + }); + + it('returns 2-item trail for page directly under a section', () => { + const trail = getBreadcrumbTrail(mockRoutes, '/develop/tools'); + + expect(trail).toEqual([{ name: 'Develop', url: 'https://docs.expo.dev' }, { name: 'Tools' }]); + }); + + it('returns 3-item trail for page nested in a group', () => { + const trail = getBreadcrumbTrail(mockRoutes, '/develop/user-interface/fonts'); + + expect(trail).toEqual([ + { name: 'Develop', url: 'https://docs.expo.dev' }, + { name: 'User interface', url: 'https://docs.expo.dev' }, + { name: 'Fonts' }, + ]); + }); + + it('last item has no url', () => { + const trail = getBreadcrumbTrail(mockRoutes, '/deploy/build-project'); + + expect(trail.at(-1)).toEqual({ name: 'Build project' }); + expect(trail.at(-1)).not.toHaveProperty('url'); + }); + + it('uses sidebarTitle over name when available', () => { + const routes: NavigationRoute[] = [ + { + type: 'section', + name: 'Section', + href: '', + children: [ + { + type: 'page', + name: 'Long Page Name', + sidebarTitle: 'Short', + href: '/section/page', + }, + ], + }, + ]; + + const trail = getBreadcrumbTrail(routes, '/section/page'); + expect(trail.at(-1)!.name).toBe('Short'); + }); + + it('skips hidden nodes', () => { + const routes: NavigationRoute[] = [ + { + type: 'section', + name: 'Section', + href: '', + children: [ + { type: 'page', name: 'Hidden', href: '/section/hidden', hidden: true }, + { type: 'page', name: 'Visible', href: '/section/visible' }, + ], + }, + ]; + + expect(getBreadcrumbTrail(routes, '/section/hidden')).toEqual([]); + expect(getBreadcrumbTrail(routes, '/section/visible')).toHaveLength(2); + }); + + it('skips null entries in route arrays', () => { + const routes = [ + null, + { + type: 'section', + name: 'Section', + href: '', + children: [{ type: 'page', name: 'Page', href: '/section/page' }], + }, + ] as unknown as NavigationRoute[]; + + const trail = getBreadcrumbTrail(routes, '/section/page'); + expect(trail).toEqual([{ name: 'Section', url: 'https://docs.expo.dev' }, { name: 'Page' }]); + }); + + it('filters out empty-name ancestors', () => { + const routes: NavigationRoute[] = [ + { + type: 'section', + name: '', + href: '', + children: [{ type: 'page', name: 'Overview', href: '/overview' }], + }, + ]; + + const trail = getBreadcrumbTrail(routes, '/overview'); + expect(trail).toEqual([{ name: 'Overview' }]); + }); + + it('falls back to docs root for sections without an index page', () => { + const routes: NavigationRoute[] = [ + { + type: 'section', + name: 'App signing', + href: '', + children: [{ type: 'page', name: 'App credentials', href: '/app-signing/app-credentials' }], + }, + ]; + + const trail = getBreadcrumbTrail(routes, '/app-signing/app-credentials'); + expect(trail[0]).toEqual({ + name: 'App signing', + url: 'https://docs.expo.dev', + }); + }); + + it('uses href directly for index page when deriving ancestor URL', () => { + const routes: NavigationRoute[] = [ + { + type: 'section', + name: 'EAS', + href: '', + children: [ + { + type: 'group', + name: 'Environment variables', + href: '', + children: [ + { + type: 'page', + name: 'Overview', + href: '/eas/environment-variables', + isIndex: true, + }, + { type: 'page', name: 'Manage', href: '/eas/environment-variables/manage' }, + ], + }, + ], + }, + ]; + + const trail = getBreadcrumbTrail(routes, '/eas/environment-variables/manage'); + expect(trail[1]).toEqual({ + name: 'Environment variables', + url: 'https://docs.expo.dev/eas/environment-variables', + }); + }); +}); diff --git a/docs/common/routes.ts b/docs/common/routes.ts index 54243e2a63fa30..1d43a1f2099afe 100644 --- a/docs/common/routes.ts +++ b/docs/common/routes.ts @@ -103,6 +103,77 @@ export const isRouteActive = ( return linkUrl === stripVersionFromPath(pathname) || linkUrl === stripVersionFromPath(asPath); }; +const DOCS_ROOT = 'https://docs.expo.dev'; + +function getAncestorUrl(node: NavigationRoute): string { + if (!node.children) { + return DOCS_ROOT; + } + + for (const child of node.children as NavigationRoute[]) { + if (!child || child.hidden) { + continue; + } + if (child.type === 'page' && child.href) { + if (child.isIndex) { + return `${DOCS_ROOT}${child.href}`; + } + } + if (child.children) { + const url = getAncestorUrl(child); + if (url !== DOCS_ROOT) { + return url; + } + } + } + return DOCS_ROOT; +} + +export function getBreadcrumbTrail( + routes: NavigationRoute[], + pathname: string +): { name: string; url?: string }[] { + const trail: { name: string; node: NavigationRoute }[] = []; + + function search(nodes: NavigationRoute[] | NavigationRouteWithSection[]): boolean { + for (const node of nodes) { + if (!node || node.hidden) { + continue; + } + + if (node.type === 'page') { + if (node.href === pathname) { + trail.push({ name: node.sidebarTitle ?? node.name, node }); + return true; + } + } else if (node.children) { + trail.push({ name: node.name, node }); + if (search(node.children)) { + return true; + } + trail.pop(); + } + } + return false; + } + + search(routes); + + return trail + .filter(item => item.name !== '') + .map((item, index, filtered) => { + const isLast = index === filtered.length - 1; + if (isLast) { + return { name: item.name }; + } + + const url = + item.node.type === 'page' ? `${DOCS_ROOT}${item.node.href}` : getAncestorUrl(item.node); + + return { name: item.name, url }; + }); +} + export function appendSectionToRoute(route?: NavigationRouteWithSection) { if (route?.children) { return route.children.map((entry: NavigationRouteWithSection) => diff --git a/docs/components/DocumentationPage.tsx b/docs/components/DocumentationPage.tsx index b03ef1406d51df..5d4cf8cf3f9fab 100644 --- a/docs/components/DocumentationPage.tsx +++ b/docs/components/DocumentationPage.tsx @@ -6,13 +6,14 @@ import { useEffect, useState, type PropsWithChildren, useRef, useCallback, useMe import { InlineHelp } from 'ui/components/InlineHelp'; import { PageHeader } from 'ui/components/PageHeader'; import * as RoutesUtils from '~/common/routes'; -import { appendSectionToRoute, isRouteActive } from '~/common/routes'; +import { appendSectionToRoute, getBreadcrumbTrail, isRouteActive } from '~/common/routes'; import { versionToText, throttle } from '~/common/utilities'; import * as WindowUtils from '~/common/window'; import DocumentationHead from '~/components/DocumentationHead'; import DocumentationNestedScrollLayout, { DocumentationNestedScrollLayoutHandles, } from '~/components/DocumentationNestedScrollLayout'; +import { buildBreadcrumbListSchema } from '~/constants/structured-data'; import { usePageApiVersion } from '~/providers/page-api-version'; import versions from '~/public/static/constants/versions.json'; import { PageMetadata } from '~/types/common'; @@ -21,6 +22,7 @@ import { Footer } from '~/ui/components/Footer'; import { Header } from '~/ui/components/Header'; import { Separator } from '~/ui/components/Separator'; import { Sidebar } from '~/ui/components/Sidebar/Sidebar'; +import { StructuredData } from '~/ui/components/StructuredData'; import { TableOfContentsHandles, TableOfContentsWithManager, @@ -61,6 +63,8 @@ export default function DocumentationPage({ const pathname = router?.pathname ?? '/'; const routes = RoutesUtils.getRoutes(pathname, version); const sidebarActiveGroup = RoutesUtils.getPageSection(pathname); + const breadcrumbTrail = getBreadcrumbTrail(routes, pathname); + const breadcrumbSchema = buildBreadcrumbListSchema(breadcrumbTrail); const sidebarScrollPosition = process?.browser ? window.__sidebarScroll : 0; const currentPath = router?.asPath ?? ''; const isLatestSdkPage = currentPath.startsWith('/versions/latest/sdk/'); @@ -310,6 +314,7 @@ export default function DocumentationPage({ onSidebarToggle={handleSidebarToggle} isSidebarCollapsed={isNavigationCollapsed} isChatExpanded={isAskAIExpanded}> + {breadcrumbSchema && } { + it('returns null for empty array', () => { + expect(buildBreadcrumbListSchema([])).toBeNull(); + }); + + it('returns null for single item', () => { + expect(buildBreadcrumbListSchema([{ name: 'Home' }])).toBeNull(); + }); + + it('returns valid BreadcrumbList for 2 items', () => { + const result = buildBreadcrumbListSchema([ + { name: 'Get started', url: 'https://docs.expo.dev/get-started' }, + { name: 'Introduction' }, + ]); + + expect(result).toEqual({ + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { + '@type': 'ListItem', + position: 1, + name: 'Get started', + item: 'https://docs.expo.dev/get-started', + }, + { '@type': 'ListItem', position: 2, name: 'Introduction' }, + ], + }); + }); + + it('returns valid BreadcrumbList for 3 items', () => { + const result = buildBreadcrumbListSchema([ + { name: 'Develop', url: 'https://docs.expo.dev/develop' }, + { name: 'User interface', url: 'https://docs.expo.dev/develop/user-interface' }, + { name: 'Fonts' }, + ]); + + expect(result?.itemListElement).toHaveLength(3); + expect(result?.itemListElement[2]).toEqual({ + '@type': 'ListItem', + position: 3, + name: 'Fonts', + }); + }); + + it('omits item property when url is undefined', () => { + const result = buildBreadcrumbListSchema([ + { name: 'Section', url: 'https://docs.expo.dev/section' }, + { name: 'Page' }, + ]); + + expect(result?.itemListElement[0]).toHaveProperty('item'); + expect(result?.itemListElement[1]).not.toHaveProperty('item'); + }); + + it('sets sequential position numbers starting at 1', () => { + const result = buildBreadcrumbListSchema([ + { name: 'A', url: 'https://example.com/a' }, + { name: 'B', url: 'https://example.com/b' }, + { name: 'C' }, + ]); + + const positions = result?.itemListElement.map((el: { position: number }) => el.position); + expect(positions).toEqual([1, 2, 3]); + }); +}); diff --git a/docs/constants/structured-data.ts b/docs/constants/structured-data.ts new file mode 100644 index 00000000000000..c8829c8e548db1 --- /dev/null +++ b/docs/constants/structured-data.ts @@ -0,0 +1,44 @@ +type BreadcrumbItem = { + name: string; + url?: string; +}; + +export function buildBreadcrumbListSchema(items: BreadcrumbItem[]) { + if (items.length < 2) { + return null; + } + + return { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: items.map((item, index) => ({ + '@type': 'ListItem', + position: index + 1, + name: item.name, + ...(item.url ? { item: item.url } : {}), + })), + }; +} + +export const websiteSchema = { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: 'Expo Documentation', + url: 'https://docs.expo.dev', + publisher: { + '@type': 'Organization', + name: 'Expo', + url: 'https://expo.dev', + logo: { + '@type': 'ImageObject', + url: 'https://docs.expo.dev/static/images/expo-logo.svg', + }, + sameAs: [ + 'https://github.com/expo', + 'https://x.com/expo', + 'https://bsky.app/profile/expo.dev', + 'https://www.linkedin.com/company/expo-dev/', + 'https://www.youtube.com/@expodevelopers', + ], + }, +}; diff --git a/docs/pages/_app.tsx b/docs/pages/_app.tsx index 1e230e85b69bc8..4e37cab2dcbd8e 100644 --- a/docs/pages/_app.tsx +++ b/docs/pages/_app.tsx @@ -10,10 +10,12 @@ import { Inter, JetBrains_Mono } from 'next/font/google'; import { preprocessSentryError } from '~/common/sentry-utilities'; import { useNProgress } from '~/common/useNProgress'; import { DocumentationPageWrapper } from '~/components/DocumentationPageWrapper'; +import { websiteSchema } from '~/constants/structured-data'; import { useAnalyticsPageTracking } from '~/providers/Analytics'; import { CodeBlockSettingsProvider } from '~/providers/CodeBlockSettingsProvider'; import { TutorialChapterCompletionProvider } from '~/providers/TutorialChapterCompletionProvider'; import { markdownComponents } from '~/ui/components/Markdown'; +import { StructuredData } from '~/ui/components/StructuredData'; import * as Tooltip from '~/ui/components/Tooltip'; import '~/common/suppress-trailing-slash-warning'; @@ -65,6 +67,7 @@ export default function App({ Component, pageProps }: AppProps) { useAnalyticsPageTracking(); return ( <> + {/* eslint-disable-next-line react/no-unknown-property */}