From 246dba458136ccf511b8a9395f507c62e4e498b9 Mon Sep 17 00:00:00 2001 From: Patryk Mleczek <67064618+pmleczek@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:02:34 +0100 Subject: [PATCH 1/2] [brownfield][cli] update copied hermes framework name to hermesvm.xcframework (#43138) # Why The hrems XCFramework names has been updated to `hermesvm.xcframework` but the CLI still copies it under `hermes.xcframework` # How Updated the name to `hermesvm.xcframework` to match the new name # Test Plan - E2E tests for CLI on the CI - Tested manually verifying compilation and name of the XCFramework # 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) - [X] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). --- packages/expo-brownfield/CHANGELOG.md | 2 ++ .../expo-brownfield/cli/build/commands/ios/build.js | 10 +++++----- packages/expo-brownfield/cli/src/commands/ios/build.ts | 10 +++++----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/expo-brownfield/CHANGELOG.md b/packages/expo-brownfield/CHANGELOG.md index 9b4b492d934eb8..41dcc0bd052b75 100644 --- a/packages/expo-brownfield/CHANGELOG.md +++ b/packages/expo-brownfield/CHANGELOG.md @@ -4,6 +4,8 @@ ### 🛠 Breaking changes +- [cli] update copied hermes framework name to hermesvm.xcframework ([#43138](https://github.com/expo/expo/pull/43138) by [@pmleczek](https://github.com/pmleczek)) + ### 🎉 New features ### 🐛 Bug fixes diff --git a/packages/expo-brownfield/cli/build/commands/ios/build.js b/packages/expo-brownfield/cli/build/commands/ios/build.js index ab61983f692748..7787fa09a1017c 100644 --- a/packages/expo-brownfield/cli/build/commands/ios/build.js +++ b/packages/expo-brownfield/cli/build/commands/ios/build.js @@ -109,17 +109,17 @@ const packageFrameworks = async (config) => { }; const copyHermesFramework = async (config) => { if (config.dryRun) { - console.log(`Copying hermes XCFramework from ${config.hermesFrameworkPath} to ${config.artifacts}/hermes.xcframework`); + console.log(`Copying hermes XCFramework from ${config.hermesFrameworkPath} to ${config.artifacts}/hermesvm.xcframework`); return; } return (0, utils_1.withSpinner)({ - operation: () => promises_1.default.cp(`./ios/${config.hermesFrameworkPath}`, `${config.artifacts}/hermes.xcframework`, { + operation: () => promises_1.default.cp(`./ios/${config.hermesFrameworkPath}`, `${config.artifacts}/hermesvm.xcframework`, { force: true, recursive: true, }), - loaderMessage: 'Copying hermes.xcframework to the artifacts directory...', - successMessage: 'Copying hermes.xcframework to the artifacts directory succeeded', - errorMessage: 'Copying hermes.xcframework to the artifacts directory failed', + loaderMessage: 'Copying hermesvm.xcframework to the artifacts directory...', + successMessage: 'Copying hermesvm.xcframework to the artifacts directory succeeded', + errorMessage: 'Copying hermesvm.xcframework to the artifacts directory failed', verbose: config.verbose, }); }; diff --git a/packages/expo-brownfield/cli/src/commands/ios/build.ts b/packages/expo-brownfield/cli/src/commands/ios/build.ts index 5f206de8309584..e2f34e6e6dd6da 100644 --- a/packages/expo-brownfield/cli/src/commands/ios/build.ts +++ b/packages/expo-brownfield/cli/src/commands/ios/build.ts @@ -135,20 +135,20 @@ const packageFrameworks = async (config: BuildConfigIos) => { const copyHermesFramework = async (config: BuildConfigIos) => { if (config.dryRun) { console.log( - `Copying hermes XCFramework from ${config.hermesFrameworkPath} to ${config.artifacts}/hermes.xcframework` + `Copying hermes XCFramework from ${config.hermesFrameworkPath} to ${config.artifacts}/hermesvm.xcframework` ); return; } return withSpinner({ operation: () => - fs.cp(`./ios/${config.hermesFrameworkPath}`, `${config.artifacts}/hermes.xcframework`, { + fs.cp(`./ios/${config.hermesFrameworkPath}`, `${config.artifacts}/hermesvm.xcframework`, { force: true, recursive: true, }), - loaderMessage: 'Copying hermes.xcframework to the artifacts directory...', - successMessage: 'Copying hermes.xcframework to the artifacts directory succeeded', - errorMessage: 'Copying hermes.xcframework to the artifacts directory failed', + loaderMessage: 'Copying hermesvm.xcframework to the artifacts directory...', + successMessage: 'Copying hermesvm.xcframework to the artifacts directory succeeded', + errorMessage: 'Copying hermesvm.xcframework to the artifacts directory failed', verbose: config.verbose, }); }; From d915858e5f70583efbec4bb6395f3f604abbe1be Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Sun, 15 Feb 2026 23:04:56 +0000 Subject: [PATCH 2/2] [ci] fix failing check-packages job (#43160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Why Commit 68f6c1ab2c changed @expo/config's evalConfig to use loadModuleSync from @expo/require-utils, which calls Node's native `require()` instead of `fs.readFileSync`. Since `require()` bypasses memfs, the 5 tests that create dynamic `app.config.js` files via vol.writeFileSync broke — the config loaded as null. Added `jest.doMock('/app/app.config.js', factory, { virtual: true })` after each vol.writeFileSync call — this registers the config as a virtual Jest module so require() can find it. Why both vol.writeFileSync and jest.doMock are needed: @expo/config uses fs.existsSync to discover config files before loading them. The memfs write satisfies file detection; the Jest mock satisfies require() loading. # How - update tests # Test Plan - green CI # 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) --- .../src/sourcer/__tests__/Expo-test.ts | 198 +++++++++--------- 1 file changed, 97 insertions(+), 101 deletions(-) diff --git a/packages/@expo/fingerprint/src/sourcer/__tests__/Expo-test.ts b/packages/@expo/fingerprint/src/sourcer/__tests__/Expo-test.ts index b21ab5348d7b58..af1da632552c14 100644 --- a/packages/@expo/fingerprint/src/sourcer/__tests__/Expo-test.ts +++ b/packages/@expo/fingerprint/src/sourcer/__tests__/Expo-test.ts @@ -29,6 +29,12 @@ jest.mock('../../ExpoResolver'); jest.mock('../../ProjectWorkflow'); jest.mock('../../utils/SpawnIPC'); +/** Write a placeholder file to memfs (for `fs.existsSync`) and register a Jest virtual mock (for `require()`). */ +function mockConfigFile(filePath: string, factory: () => any) { + vol.writeFileSync(filePath, ''); + jest.doMock(filePath, factory, { virtual: true }); +} + // NOTE(cedric): this is a workaround to also mock `node:fs` jest.mock('node:fs', () => require('memfs').fs); @@ -410,45 +416,43 @@ describe(`getExpoConfigSourcesAsync - sourceSkips`, () => { }); it('should not container version when SourceSkips.ExpoConfigVersions', async () => { - vol.fromJSON(require('./fixtures/ExpoManaged47Project.json')); - vol.writeFileSync( - '/app/app.config.js', - `\ -export default ({ config }) => { - config.android = { versionCode: 1 }; - config.ios = { buildNumber: '1' }; - return config; -};` - ); + await jest.isolateModulesAsync(async () => { + vol.fromJSON(require('./fixtures/ExpoManaged47Project.json')); + mockConfigFile('/app/app.config.js', () => ({ + default: ({ config }: any) => { + config.android = { versionCode: 1 }; + config.ios = { buildNumber: '1' }; + return config; + }, + })); - const options = await normalizeOptionsAsync('/app', { - sourceSkips: SourceSkips.ExpoConfigVersions, + const options = await normalizeOptionsAsync('/app', { + sourceSkips: SourceSkips.ExpoConfigVersions, + }); + const { config, loadedModules } = await getExpoConfigAsync('/app', options); + const sources = await getExpoConfigSourcesAsync('/app', config, loadedModules, options); + const expoConfigSource = sources.find( + (source): source is HashSourceContents => + source.type === 'contents' && source.id === 'expoConfig' + ); + const expoConfig = JSON.parse(expoConfigSource?.contents?.toString() ?? 'null'); + expect(expoConfig).not.toBeNull(); + expect(expoConfig.version).toBeUndefined(); + expect(expoConfig.android.versionCode).toBeUndefined(); + expect(expoConfig.ios.buildNumber).toBeUndefined(); }); - const { config, loadedModules } = await getExpoConfigAsync('/app', options); - const sources = await getExpoConfigSourcesAsync('/app', config, loadedModules, options); - const expoConfigSource = sources.find( - (source): source is HashSourceContents => - source.type === 'contents' && source.id === 'expoConfig' - ); - const expoConfig = JSON.parse(expoConfigSource?.contents?.toString() ?? 'null'); - expect(expoConfig).not.toBeNull(); - expect(expoConfig.version).toBeUndefined(); - expect(expoConfig.android.versionCode).toBeUndefined(); - expect(expoConfig.ios.buildNumber).toBeUndefined(); }); it('should support sourceSkips from config', async () => { await jest.isolateModulesAsync(async () => { vol.fromJSON(require('./fixtures/ExpoManaged47Project.json')); - vol.writeFileSync( - '/app/app.config.js', - `\ -export default ({ config }) => { - config.android = { versionCode: 1, package: 'com.example.app' }; - config.ios = { buildNumber: '1', bundleIdentifier: 'com.example.app' }; - return config; -};` - ); + mockConfigFile('/app/app.config.js', () => ({ + default: ({ config }: any) => { + config.android = { versionCode: 1, package: 'com.example.app' }; + config.ios = { buildNumber: '1', bundleIdentifier: 'com.example.app' }; + return config; + }, + })); const configContents = `\ const { SourceSkips } = require('@expo/fingerprint'); @@ -458,10 +462,7 @@ const config = { }; module.exports = config; `; - vol.writeFileSync('/app/fingerprint.config.js', configContents); - jest.doMock('/app/fingerprint.config.js', () => requireString(configContents), { - virtual: true, - }); + mockConfigFile('/app/fingerprint.config.js', () => requireString(configContents)); const options = await normalizeOptionsAsync('/app'); const { config, loadedModules } = await getExpoConfigAsync('/app', options); @@ -483,15 +484,13 @@ module.exports = config; it('should support sourceSkips specified as string array in config', async () => { await jest.isolateModulesAsync(async () => { vol.fromJSON(require('./fixtures/ExpoManaged47Project.json')); - vol.writeFileSync( - '/app/app.config.js', - `\ -export default ({ config }) => { - config.android = { versionCode: 1, package: 'com.example.app' }; - config.ios = { buildNumber: '1', bundleIdentifier: 'com.example.app' }; - return config; -};` - ); + mockConfigFile('/app/app.config.js', () => ({ + default: ({ config }: any) => { + config.android = { versionCode: 1, package: 'com.example.app' }; + config.ios = { buildNumber: '1', bundleIdentifier: 'com.example.app' }; + return config; + }, + })); const configContents = `\ const { SourceSkips } = require('@expo/fingerprint'); @@ -501,10 +500,7 @@ const config = { }; module.exports = config; `; - vol.writeFileSync('/app/fingerprint.config.js', configContents); - jest.doMock('/app/fingerprint.config.js', () => requireString(configContents), { - virtual: true, - }); + mockConfigFile('/app/fingerprint.config.js', () => requireString(configContents)); const options = await normalizeOptionsAsync('/app'); const { config, loadedModules } = await getExpoConfigAsync('/app', options); @@ -524,63 +520,63 @@ module.exports = config; }); it('should not contain runtimeVersion when SourceSkips.ExpoConfigRuntimeVersionIfString and runtime version is a string', async () => { - vol.fromJSON(require('./fixtures/ExpoManaged47Project.json')); - vol.writeFileSync( - '/app/app.config.js', - `\ -export default ({ config }) => { - config.runtimeVersion = '1.0.0'; - config.ios = { runtimeVersion: '1.0.0' }; - config.android = { runtimeVersion: '1.0.0' }; - config.web = { runtimeVersion: '1.0.0' }; - return config; -};` - ); - const options = await normalizeOptionsAsync('/app', { - sourceSkips: SourceSkips.ExpoConfigRuntimeVersionIfString, + await jest.isolateModulesAsync(async () => { + vol.fromJSON(require('./fixtures/ExpoManaged47Project.json')); + mockConfigFile('/app/app.config.js', () => ({ + default: ({ config }: any) => { + config.runtimeVersion = '1.0.0'; + config.ios = { runtimeVersion: '1.0.0' }; + config.android = { runtimeVersion: '1.0.0' }; + config.web = { runtimeVersion: '1.0.0' }; + return config; + }, + })); + const options = await normalizeOptionsAsync('/app', { + sourceSkips: SourceSkips.ExpoConfigRuntimeVersionIfString, + }); + const { config, loadedModules } = await getExpoConfigAsync('/app', options); + const sources = await getExpoConfigSourcesAsync('/app', config, loadedModules, options); + const expoConfigSource = sources.find( + (source): source is HashSourceContents => + source.type === 'contents' && source.id === 'expoConfig' + ); + const expoConfig = JSON.parse(expoConfigSource?.contents?.toString() ?? 'null'); + expect(expoConfig).not.toBeNull(); + expect(expoConfig.runtimeVersion).toBeUndefined(); + expect(expoConfig.android.runtimeVersion).toBeUndefined(); + expect(expoConfig.ios.runtimeVersion).toBeUndefined(); + expect(expoConfig.web.runtimeVersion).toBeUndefined(); }); - const { config, loadedModules } = await getExpoConfigAsync('/app', options); - const sources = await getExpoConfigSourcesAsync('/app', config, loadedModules, options); - const expoConfigSource = sources.find( - (source): source is HashSourceContents => - source.type === 'contents' && source.id === 'expoConfig' - ); - const expoConfig = JSON.parse(expoConfigSource?.contents?.toString() ?? 'null'); - expect(expoConfig).not.toBeNull(); - expect(expoConfig.runtimeVersion).toBeUndefined(); - expect(expoConfig.android.runtimeVersion).toBeUndefined(); - expect(expoConfig.ios.runtimeVersion).toBeUndefined(); - expect(expoConfig.web.runtimeVersion).toBeUndefined(); }); it('should keep runtimeVersion when SourceSkips.ExpoConfigRuntimeVersionIfString and runtime version is a policy', async () => { - vol.fromJSON(require('./fixtures/ExpoManaged47Project.json')); - vol.writeFileSync( - '/app/app.config.js', - `\ -export default ({ config }) => { - config.runtimeVersion = { policy: 'test' }; - config.ios = { runtimeVersion: { policy: 'test' } }; - config.android = { runtimeVersion: { policy: 'test' } }; - config.web = { runtimeVersion: { policy: 'test' } }; - return config; -};` - ); - const options = await normalizeOptionsAsync('/app', { - sourceSkips: SourceSkips.ExpoConfigRuntimeVersionIfString, + await jest.isolateModulesAsync(async () => { + vol.fromJSON(require('./fixtures/ExpoManaged47Project.json')); + mockConfigFile('/app/app.config.js', () => ({ + default: ({ config }: any) => { + config.runtimeVersion = { policy: 'test' }; + config.ios = { runtimeVersion: { policy: 'test' } }; + config.android = { runtimeVersion: { policy: 'test' } }; + config.web = { runtimeVersion: { policy: 'test' } }; + return config; + }, + })); + const options = await normalizeOptionsAsync('/app', { + sourceSkips: SourceSkips.ExpoConfigRuntimeVersionIfString, + }); + const { config, loadedModules } = await getExpoConfigAsync('/app', options); + const sources = await getExpoConfigSourcesAsync('/app', config, loadedModules, options); + const expoConfigSource = sources.find( + (source): source is HashSourceContents => + source.type === 'contents' && source.id === 'expoConfig' + ); + const expoConfig = JSON.parse(expoConfigSource?.contents?.toString() ?? 'null'); + expect(expoConfig).not.toBeNull(); + expect(expoConfig.runtimeVersion).toMatchObject({ policy: 'test' }); + expect(expoConfig.android.runtimeVersion).toMatchObject({ policy: 'test' }); + expect(expoConfig.ios.runtimeVersion).toMatchObject({ policy: 'test' }); + expect(expoConfig.web.runtimeVersion).toMatchObject({ policy: 'test' }); }); - const { config, loadedModules } = await getExpoConfigAsync('/app', options); - const sources = await getExpoConfigSourcesAsync('/app', config, loadedModules, options); - const expoConfigSource = sources.find( - (source): source is HashSourceContents => - source.type === 'contents' && source.id === 'expoConfig' - ); - const expoConfig = JSON.parse(expoConfigSource?.contents?.toString() ?? 'null'); - expect(expoConfig).not.toBeNull(); - expect(expoConfig.runtimeVersion).toMatchObject({ policy: 'test' }); - expect(expoConfig.android.runtimeVersion).toMatchObject({ policy: 'test' }); - expect(expoConfig.ios.runtimeVersion).toMatchObject({ policy: 'test' }); - expect(expoConfig.web.runtimeVersion).toMatchObject({ policy: 'test' }); }); it('should skip external icon files when SourceSkips.ExpoConfigAssets', async () => {