Skip to content

fix(link-assets): match asset extensions case-insensitively - #2847

Open
ahmdshrif wants to merge 1 commit into
react-native-community:mainfrom
ahmdshrif:fix-link-assets-uppercase-extension
Open

fix(link-assets): match asset extensions case-insensitively#2847
ahmdshrif wants to merge 1 commit into
react-native-community:mainfrom
ahmdshrif:fix-link-assets-uppercase-extension

Conversation

@ahmdshrif

Copy link
Copy Markdown

Summary

link-assets decides how to link each asset by bucketing it on its file extension, in packages/cli-link-assets/src/tools/linkPlatform/index.ts:

filter: (asset) => path.extname(asset.path) === `.${fileExt}`,

fileExt comes from the lower-case lists in fileTypes.ts (otf, ttf, png, jpg, gif, mp3), so the comparison is case-sensitive. An asset whose extension is cased differently on disk — Lato-Regular.TTF, Photo.PNG, Clip.MP3 — matches no bucket and falls through to the custom one, which is meant for unknown file types.

That is a silent mislink, not an error:

  • Android — the font is copied to app/src/main/assets/custom/ instead of app/src/main/res/font/; no res/font/<family>.xml is generated and no ReactFontManager.getInstance().addCustomFont(...) call is inserted into MainApplication. An image lands in the same folder instead of res/drawable/.
  • iOS — the file is still added to the Xcode Resources group, but copyAssets receives isFontAsset: false, so it is never appended to UIAppFonts in Info.plist. The font is inside the app bundle and cannot be used at runtime.

Uppercase font extensions are common (a lot of foundries and font sites ship .TTF/.OTF), and this package's own README documents the supported types as "Fonts (OTF, TTF)" and "Images (JPG, PNG, GIF)", so .TTF is a reasonable thing for a user to have in assets/.

The same case-sensitive comparison is in tools/manifest/migrations/migration2.ts, which decides whether a previously linked asset is a font that needs relinking to XML resources, so an uppercase-extension font in an old manifest is skipped by that migration too.

This normalizes the extension once, in a getAssetExtension() helper next to the type lists, and uses it at both comparison sites. Assets whose extension is already lower-case are completely unaffected.

Test Plan

Two tests were added to packages/cli-link-assets/src/__tests__/linkAssets.test.ts, following the existing end-to-end pattern (a real temp project, real font fixtures, linkAssets() run for real):

  • should link a font asset whose extension is upper-cased — links assets/shared/fonts/Montserrat-Regular.TTF and asserts that res/font/montserrat.xml references it, that res/font/montserrat_regular.ttf exists, that it is not in assets/custom/, that MainApplication.kt gets the addCustomFont call, and that Info.plist lists it under UIAppFonts.
  • should link an image asset whose extension is upper-cased — links assets/shared/Upper Image.PNG and asserts it ends up in res/drawable/upper_image.png and not in assets/custom/.

Before the fix (tests applied, sources unchanged) both fail, showing the asset in the wrong place:

✕ should link a font asset whose extension is upper-cased
  ENOENT: no such file or directory, open '.../android/app/src/main/res/font/montserrat.xml'
✕ should link an image asset whose extension is upper-cased
  expect(received).toBe(expected)  // res/drawable/upper_image.png
  Expected: true
  Received: false

  (instrumented run)
  CUSTOM:  [ 'Montserrat-Regular.TTF', 'TestSample Document.pdf' ]
  RESFONT: []
  UIAppFonts contains TTF? false

Tests: 2 failed, 6 passed, 8 total

After the fix:

Tests:       8 passed, 8 total
Snapshots:   32 passed, 32 total

All 32 pre-existing snapshots pass unchanged, which is the regression guard for lower-case extensions.

Whole repo:

  • node_modules/.bin/jest packages55 suites, 315 passed, 1 todo
  • node ./scripts/buildTs.js → clean
  • node_modules/.bin/eslint packages/cli-link-assets/src --ext .ts → clean

Checklist

  • Documentation is up to date.
  • Follows commit message convention described in CONTRIBUTING.md.
  • For functional changes, my test plan has linked these CLI changes into a local react-native checkout (instructions).

Note on the last box: verification here was the package's own end-to-end test suite, which drives linkAssets() against a real temporary Android/iOS project fixture rather than a linked react-native checkout. Happy to run the linked-checkout test plan if you'd like that on the record.

`link-assets` dispatches every asset to a per-extension bucket by
comparing `path.extname()` against the lower-case type lists in
`fileTypes.ts`. The comparison is case-sensitive, so an asset whose
extension is cased differently on disk — `Lato-Regular.TTF`,
`Photo.PNG` — never matches its bucket and falls through to the
"custom" one instead.

The result is a silent mislink:

- on Android the font is copied to `app/src/main/assets/custom/`
  rather than `app/src/main/res/font/`, no `res/font/<family>.xml` is
  generated and no `ReactFontManager.addCustomFont()` call is added to
  `MainApplication`, and an image lands there instead of
  `res/drawable/`;
- on iOS the file is added to the Xcode `Resources` group but
  `isFontAsset` is false, so it is never appended to `UIAppFonts` and
  the font ships inside the bundle without being loadable at runtime.

Normalize the extension once, in `getAssetExtension()`, and use it both
where `linkPlatform` builds its filters and in `migration2`, which
flags previously linked fonts for relinking with the same comparison.

Extensions that were already lower-case are unaffected: all 32 existing
snapshots pass unchanged.
@ahmdshrif
ahmdshrif requested a review from thymikee as a code owner August 30, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant