From 81b4e8f29c2272261497dc7beeca56494bda47cc Mon Sep 17 00:00:00 2001 From: Cristian Ceban <58264709+CristiCeban@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:51:13 +0300 Subject: [PATCH] fix(jpeg): android jpeg drawable resources --- .changeset/sharp-jpegs-draw.md | 7 ++++ .../src/loaders/assetsLoader/assetsLoader.ts | 2 +- .../__fixtures__/assets/TestPhoto@1x.jpeg | 1 + .../__fixtures__/assets/TestPhoto@2x.jpeg | 1 + .../__fixtures__/assets/TestPhoto@3x.jpeg | 1 + .../src/loaders/assets-loader.test.ts | 37 +++++++++++++++++++ .../src/latest/api/loaders/assets-loader.md | 14 +++---- 7 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 .changeset/sharp-jpegs-draw.md create mode 100644 tests/integration/src/loaders/__fixtures__/assets/TestPhoto@1x.jpeg create mode 100644 tests/integration/src/loaders/__fixtures__/assets/TestPhoto@2x.jpeg create mode 100644 tests/integration/src/loaders/__fixtures__/assets/TestPhoto@3x.jpeg diff --git a/.changeset/sharp-jpegs-draw.md b/.changeset/sharp-jpegs-draw.md new file mode 100644 index 000000000..832be76b4 --- /dev/null +++ b/.changeset/sharp-jpegs-draw.md @@ -0,0 +1,7 @@ +--- +"@callstack/repack": patch +--- + +Fix `.jpeg` images not showing in Android release builds. They were emitted to `raw` instead of `drawable-*`, where React Native looks for them. + +If you have a `.jpeg` and a `.png`/`.jpg`/`.gif`/`.webp` image with the same name in the same directory (e.g. `logo.png` and `logo.jpeg`), rename one of them. Both now resolve to the same Android drawable resource name, and the build fails with a duplicate resources error. diff --git a/packages/repack/src/loaders/assetsLoader/assetsLoader.ts b/packages/repack/src/loaders/assetsLoader/assetsLoader.ts index 3c4724fdd..2688012ad 100644 --- a/packages/repack/src/loaders/assetsLoader/assetsLoader.ts +++ b/packages/repack/src/loaders/assetsLoader/assetsLoader.ts @@ -15,7 +15,7 @@ export const raw = true; const testXml = /\.(xml)$/; const testMP4 = /\.(mp4)$/; -const testImages = /\.(png|jpg|gif|webp)$/; +const testImages = /\.(png|jpg|jpeg|gif|webp)$/; const testFonts = /\.(ttf|otf|ttc)$/; export default async function repackAssetsLoader( diff --git a/tests/integration/src/loaders/__fixtures__/assets/TestPhoto@1x.jpeg b/tests/integration/src/loaders/__fixtures__/assets/TestPhoto@1x.jpeg new file mode 100644 index 000000000..ac84a840b --- /dev/null +++ b/tests/integration/src/loaders/__fixtures__/assets/TestPhoto@1x.jpeg @@ -0,0 +1 @@ +image fixture jpeg @1x diff --git a/tests/integration/src/loaders/__fixtures__/assets/TestPhoto@2x.jpeg b/tests/integration/src/loaders/__fixtures__/assets/TestPhoto@2x.jpeg new file mode 100644 index 000000000..c0973a6bc --- /dev/null +++ b/tests/integration/src/loaders/__fixtures__/assets/TestPhoto@2x.jpeg @@ -0,0 +1 @@ +image fixture jpeg @2x diff --git a/tests/integration/src/loaders/__fixtures__/assets/TestPhoto@3x.jpeg b/tests/integration/src/loaders/__fixtures__/assets/TestPhoto@3x.jpeg new file mode 100644 index 000000000..a71628f02 --- /dev/null +++ b/tests/integration/src/loaders/__fixtures__/assets/TestPhoto@3x.jpeg @@ -0,0 +1 @@ +image fixture jpeg @3x diff --git a/tests/integration/src/loaders/assets-loader.test.ts b/tests/integration/src/loaders/assets-loader.test.ts index 76eca6f71..40d1a6c29 100644 --- a/tests/integration/src/loaders/assets-loader.test.ts +++ b/tests/integration/src/loaders/assets-loader.test.ts @@ -176,6 +176,43 @@ describe('assetLoader', () => { } ); + it('should emit scaled .jpeg images to matching drawable-* folders', async () => { + const { code, volume } = await compileBundle( + 'android', + { + ...getReactNativeVirtualModules(), + './index.js': + "export { default } from './__fixtures__/assets/TestPhoto.jpeg';", + }, + false, + undefined, + undefined, + { extensions: ['jpeg'] } + ); + + const context: { Export?: { default: Record } } = {}; + vm.runInNewContext(code, context); + + expect(context.Export?.default).toMatchObject({ + name: 'TestPhoto', + type: 'jpeg', + scales: [1, 2, 3], + }); + for (const [scale, destination] of [ + [1, 'drawable-mdpi'], + [2, 'drawable-xhdpi'], + [3, 'drawable-xxhdpi'], + ]) { + expect( + volume.readFileSync( + `/out/${destination}/__fixtures___assets_testphoto.jpeg`, + 'utf8' + ) + ).toBe(`image fixture jpeg @${scale}x\n`); + } + expect(volume.existsSync('/out/raw')).toBe(false); + }); + describe.each([ { mode: 'iOS', platform: 'ios', devServer: false, remote: false }, { diff --git a/website/src/latest/api/loaders/assets-loader.md b/website/src/latest/api/loaders/assets-loader.md index 9d841ffd4..ced15e18e 100644 --- a/website/src/latest/api/loaders/assets-loader.md +++ b/website/src/latest/api/loaders/assets-loader.md @@ -10,13 +10,13 @@ By default, extracted asset files are copied to `assets/` directory for iOS and When bundling for Android without the development server, extracted assets are placed in the following resource directories: -| Asset | Output directory | -| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| Images (`png`, `jpg`, `gif`, `webp`) | `drawable-*` (e.g. `drawable-mdpi`, `drawable-hdpi`) per scale | -| Font binaries (`ttf`, `otf`, `ttc`) | `font` **and** `raw` | -| XML font families (``) | `font` | -| Other XML files | `drawable-*` | -| Everything else (other image formats such as `jpeg` or `svg`, video, audio, other files) | `raw` | +| Asset | Output directory | +| --------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| Images (`png`, `jpg`, `jpeg`, `gif`, `webp`) | `drawable-*` (e.g. `drawable-mdpi`, `drawable-hdpi`) per scale | +| Font binaries (`ttf`, `otf`, `ttc`) | `font` **and** `raw` | +| XML font families (``) | `font` | +| Other XML files | `drawable-*` | +| Everything else (other image formats such as `bmp` or `svg`, video, audio, other files) | `raw` | `xml` and `ttc` are not included in the default [asset extensions](/api/utils/constants#asset_extensions), so `getAssetTransformRules()` doesn't process them. To bundle them, add a rule that uses the Assets loader for these extensions.