Skip to content

feat(ios): make CoreSVG decoder opt-in - #606

Open
oleksandrzavarzin-callstack wants to merge 11 commits into
callstack:mainfrom
oleksandrzavarzin-callstack:codex/issue-565-svg-opt-in
Open

oleksandrzavarzin-callstack wants to merge 11 commits into
callstack:mainfrom
oleksandrzavarzin-callstack:codex/issue-565-svg-opt-in

Conversation

@oleksandrzavarzin-callstack

@oleksandrzavarzin-callstack oleksandrzavarzin-callstack commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Closes #565. It is a continuation of @thiagobrez's work from #566.

Summary

The private CoreSVG code on Apple platforms can now be removed from the build. SVG decoding stays enabled by default in this release. The next major version disables it by default.

$RNBottomTabsEnableSVG / Expo enableSVG / SwiftPM RN_BOTTOM_TABS_ENABLE_SVG This release Next major
not set enabled, with a deprecation warning disabled
false / false / 0 disabled, no private CoreSVG code compiled disabled
true / true / 1 enabled enabled
 SvgDecoder (always compiled and registered, as on main)
   canDecodeImageData:
+    if SVG is disabled → NO, and a dev warning that names the flag
 CoreSVG.mm
+  #if RN_BOTTOM_TABS_ENABLE_SVG   # dlsym calls and base64 strings
 svg_configuration.rb / Package.swift
+  read the flag, set the define, warn when the flag is not set

Changes on top of #566:

  • SwiftPM opt-in in Package.swift. SvgDecoder.h is removed from ios/include/module.modulemap.
  • The verification artifacts (artifacts/issue-565/) are removed from the repository.
  • TabViewProvider.swift is split for SwiftLint. The SVG helpers moved to ios/SVGIconLoader.swift.

Results

Tested with Xcode 27.0 on an iPhone simulator with iOS 26.5.

Flag is not set 0
CoreSVGWrapper and base64 literals in the binary present none
SVG tab icon renders title only
Plain <Image> with an SVG source onLoad onError
Dev warning in React Native DevTools "…disabled by default in the next major version…" "SVG decoding is disabled…"
pod install warning once none
  • yarn test: passes, with 6 Ruby tests for the flag and 5 tests for the Expo Podfile helper.
  • yarn lint and yarn typecheck: pass.

Before Merging

Blast Radius: none by default

Apps that do not set the flag keep the current behavior and get a deprecation warning. Only apps that set the flag to false lose SVG images, on purpose. Android is not changed.

@thiagobrez

Copy link
Copy Markdown
Member

Hey @oleksandrzavarzin-callstack, thanks for picking this up from #566. The codegen, modulemap and SPM work is solid. I'm asking for a change of direction on two points, plus a few smaller findings.

1. Let's not ship a breaking change now

Keep SVG enabled by default in this release, and make disabling it the opt-in. Flipping the default should wait for the next major.

Suggested shape: a single tri-state flag, so the next major doesn't need a new one:

$RNBottomTabsEnableSVG / Expo enableSVG / SPM RN_BOTTOM_TABS_ENABLE_SVG This release Next major
unset enabled, with a deprecation notice disabled
false / false / 0 disabled, no private API compiled disabled
true / true / 1 enabled, notice silenced enabled

Please flag the upcoming change in these places:

  • Pod::UI.warn during pod install when the flag is unset, e.g. "SVG tab icons will be disabled by default in the next major. Set $RNBottomTabsEnableSVG = true to keep them, or false to drop the private CoreSVG code now."
  • A deprecation callout in the docs section.
  • Make the changeset minor, and mention the upcoming default change in its note.
  • A TODO(next major) next to the default in svg_configuration.rb and Package.swift.

2. <Image> SVGs must keep working

On main, SvgDecoder is registered as a global RCTImageDataDecoder, so any <Image source={...svg}> in a consumer app works because of this library. This PR scopes the decoder to tab icons only, which silently breaks those images even with the flag on. We can't break unrelated parts of someone's app as a side effect. When SVG is enabled, the behavior must stay exactly what it is on main.

Since codegenConfig is static, I'd suggest:

  • Keep the modulesConformingToProtocol registration, and keep SvgDecoder always compiled.
  • Gate only the private parts (the body of CoreSVG.mm, the base64 strings, the dlsym calls) behind RN_BOTTOM_TABS_ENABLE_SVG. When disabled, canDecodeImageData: returns NO.
  • Don't drop the class itself while the registration stays. RCTTurboModuleManager -moduleForName: calls RCTLogError(@"Unable to find module for %@") on lookup failure, which is a red box in dev.

With SVG disabled, the binary then has no private API references and no base64 literals, only a no-op SvgDecoder. That addresses #565's actual concern.

This also lets us drop SVGIconLoader and the TabViewProvider changes entirely, since RCTImageLoader decodes SVGs again. That resolves findings 3 and 4 below. The example app changes that replaced SVG icons with PNGs can be reverted too.

Other findings

  1. Silent failure when SVG is disabled. If a tab icon is an SVG, the only signal is a Swift print, which never reaches Metro or LogBox. Please add a dev-mode RCTLogWarn that points to the flag. This matters more once the default flips.
  2. CI only builds one configuration. No job builds the non-default side of the flag, so that code can rot unnoticed. Please add a CI variant for it, for both CocoaPods and SPM.
  3. The fallback refetches every failed image (moot if SVGIconLoader goes away). With the flag on, any RCTImageLoader error (404, offline, a custom scheme) triggers a second download through URLSession.
  4. SVG requests bypass RN's network stack (moot if SVGIconLoader goes away). They skip RCTImageLoader's cache, custom RCTImageURLLoaders and request handlers, and network inspection.
  5. The flag check is too strict. The podspec only accepts == true, so '1' or 'true' are silently ignored. With the tri-state, explicit false also needs handling. Consider having the podspec read ENV['RN_BOTTOM_TABS_ENABLE_SVG'] too, so CocoaPods and SPM share one mechanism.
  6. The bridging header edit does nothing. react-native-bottom-tabs-Bridging-Header.h isn't wired into the podspec and is excluded in Package.swift.
  7. The Expo plugin uses a deep import (minor). @expo/config-plugins/build/utils/generateCode is an internal path, and mergeContents throws if a custom Podfile has no platform :ios line.

@oleksandrzavarzin-callstack

Copy link
Copy Markdown
Contributor Author

Thanks @thiagobrez! I've done:

  • SVG stays enabled by default. The flag is tri-state, as in your table. If it is not set, pod install shows a deprecation warning.
  • SvgDecoder is registered as on main. Only the private CoreSVG code is behind the flag, so <Image> SVGs work again.
  • SVGIconLoader and the TabViewProvider changes are removed.
  • A dev warning names the flag when SVG is disabled and the app loads an SVG.
  • The podspec also reads RN_BOTTOM_TABS_ENABLE_SVG, and the Expo plugin no longer uses the internal import.
  • New CI jobs build with SVG disabled and check that no CoreSVG code is in the binary

@thiagobrez

Copy link
Copy Markdown
Member

@apex-sentinel-staging review

@thiagobrez

Copy link
Copy Markdown
Member

/review

@apex-sentinel-staging

Copy link
Copy Markdown

Apex Sentinel

Verdict: approve

11 finding(s): 6 minor, 5 nit. Verdict: approve. reached max rounds (1).

🟡 Minor (6)

Where Finding
packages/react-native-bottom-tabs/Package.swift:10 SwiftPM accepts only 0/false/no and silently enables on anything else, unlike the Ruby path which raises
packages/react-native-bottom-tabs/package.json:41 test script now hard-depends on ruby, breaking yarn test where Ruby is absent
packages/react-native-bottom-tabs/src/expoPodfile.ts:14 setPodfileSVGFlag leaves a duplicated flag when the generated block's end marker is missing
packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm:26 The dev warning is sent by invoking the JS module named "RCTLog" with method "logToConsole", but RCTLog is not a call…
packages/react-native-bottom-tabs/src/expo.ts:63 Prebuild deletes a hand-set $RNBottomTabsEnableSVG block for consumers who leave enableSVG unset
packages/react-native-bottom-tabs/src/expoPodfile.ts:24 Expo config plugin never sets RN_BOTTOM_TABS_ENABLE_SVG, so enableSVG: true still gets the deprecation warning
🔵 Nit (5)
Where Finding
packages/react-native-bottom-tabs/Package.swift:83 Duplicated filter(\.enabled).map define expression and diverging whitespace handling between Package.swift and svg_configuration.rb
packages/react-native-bottom-tabs/Package.swift:5 Package.swift comment documents a different set of accepted values than the code it sits above
packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm:22 @synthesize callableJSModules and RCTCallableJSModules rely on headers SvgDecoder.mm never imports
packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm:22 @synthesize callableJSModules sits outside the #if RCT_DEV guard that covers its only reader
packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm:26 logDevWarning silently drops the deprecation warning when callableJSModules is nil in a Release build

Reply under an inline comment with @apex-sentinel-staging note ... or @apex-sentinel-staging pivot ... to re-check that finding.
Comment @apex-sentinel-staging review on this conversation to run a full review.

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 11 new findings on da9795f. The verdict and the full list are in the summary comment.

// unset -> enabled, with a deprecation notice in dev builds
// 1 / true -> enabled
// 0 / false -> disabled, no private CoreSVG code is compiled
let svgSetting = Context.environment["RN_BOTTOM_TABS_ENABLE_SVG"]?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor · SwiftPM accepts only 0/false/no and silently enables on anything else, unlike the Ruby path which raises

enableSVG = !["0", "false", "no"].contains(svgSetting ?? "") (Package.swift:15) treats every other non-empty value as "enabled", and because svgSettingIsUnset is false in that case RN_BOTTOM_TABS_SVG_DEFAULTED is not defined either — so there is no deprecation notice and no error. RN_BOTTOM_TABS_ENABLE_SVG=off (or a typo such as flase, or yes) therefore leaves the private CoreSVG dlsym calls and base64 literals in the binary with no signal at all, while the identical value under CocoaPods hard-fails pod install with ArgumentError (svg_configuration.rb:28-33). The two readers of the same variable also disagree on the accepted vocabulary: the Ruby side accepts yes as true, the SwiftPM side does not.

Suggested change

Use one accepted-value set in both readers (true/1/yes, false/0/no, anything else is an error) and make Package.swift fail the resolution with a clear message — e.g. preprocessWarning/a forced fatalError-style diagnostics — instead of defaulting to enabled for a value the Podfile path rejects.

Details

Also raised here

Also raised at this location by javascript: SwiftPM accepts flag values that the Podfile path rejects as an error — Package.swift enables SVG for anything that is not exactly 0/false/no after lowercasing and whitespace removal, so a typo or an unexpected value (RN_BOTTOM_TABS_ENABLE_SVG=flase, off, no , 2) silently produces an SVG-enabled build — i.e. the private CoreSVG code the author asked to remove stays linked, with no diagnostic. The CocoaPods path in ios/svg_configuration.rb treats exactly the same input as a hard error (raise ArgumentError), so the two flag readers disagree: the same environment value either aborts pod install or silently changes the binary depending on which build system the app uses. Note svgSettingIsUnset also only checks isEmpty, so a whitespace-only value (' ') is treated as unset in the Ruby path (strip → nil → DEFAULTED warning) and coincidentally matches here, but any other non-empty unknown value falls through to "enabled".

Also raised at this location by senior-developer: SwiftPM and CocoaPods disagree on an unsupported flag value: env var silently enables SVG, Podfile hard-fails pod install — Package.swift:15 decides with !=["0", "false", "no"].contains(svgSetting ?? ""), i.e. anything that is not exactly one of those three strings enables the decoder, while svg_configuration.rb:22-34 raises ArgumentError for anything that is not in TRUE_VALUES/FALSE_VALUES/empty. Setting RN_BOTTOM_TABS_ENABLE_SVG=true gives the same result in both paths, but a typo such as RN_BOTTOM_TABS_ENABLE_SVG=ture (or enabled, OFF) silently enables the private CoreSVG code for an app that intended to remove it — exactly the outcome the flag exists to guarantee, and the CI assertion Check that no private CoreSVG code is linked would only catch it in the job that sets the value itself. The same typo in $RNBottomTabsEnableSVG aborts pod install with a clear ArgumentError. …

Also raised at this location by maintainability: SwiftPM silently enables SVG for an unrecognized flag value where the podspec raises — enableSVG = !["0", "false", "no"].contains(svgSetting ?? "") only matches the negative list, so a typo such as RN_BOTTOM_TABS_ENABLE_SVG=flase resolves to enabled and compiles the private CoreSVG code into the app, the opposite of what the author asked for. Worse, .filter { !$0.isWhitespace } strips whitespace from anywhere in the value instead of trimming it, so "0 1" becomes "01" and is enabled too. svg_configuration.rb#setting raises ArgumentError for exactly this input (locked in by test_unsupported_value_raises), so the two configuration paths disagree on the same bad value. Validate against explicit true/false lists and preconditionFailure on anything else.

persona: swift-ios, javascript, senior-developer, maintainability

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apex-sentinel-staging like

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

],
"scripts": {
"test": "jest",
"test": "ruby ios/__tests__/svg_configuration_test.rb && jest",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor · test script now hard-depends on ruby, breaking yarn test where Ruby is absent

packages/react-native-bottom-tabs/package.json:41 changes "test": "jest" to "test": "ruby ios/__tests__/svg_configuration_test.rb && jest". Ruby is not a devDependency of this repo, it is not declared in any engines/CI requirement for the jobs that run the tests, and it is not present in every environment the package is tested in (in this very review sandbox ruby is not on PATH: bash: line 1: ruby: command not found). Because of &&, a missing ruby binary aborts the whole script before jest runs, so all JS tests silently stop running: the failure mode is the most alarming one (the JS suite never executes) and a contributor or CI runner on a Ruby-less image (a plain Node container, Windows without RubyInstaller, a sandboxed CI image) will get "ruby: command not found" and no JS coverage at all. There is also no Gemfile/gemspec declaring the minitest dependency the new test requires. Suggested fix: run the Ruby suite from a dedicated job/step guarded by a Ruby setup action (or a test:ruby script the CI matrix invokes) so yarn test stays runnable with only Node, and add the Ruby/minitest provisioning to CI explicitly.

Suggested change
"test": "ruby ios/__tests__/svg_configuration_test.rb && jest",
"test": "jest",
"test:ruby": "ruby ios/__tests__/svg_configuration_test.rb",

Suggested change

Move the Ruby suite out of the test script (e.g. a separate test:ruby script invoked by a CI job that installs Ruby), so yarn test keeps working where Ruby is not installed, and declare the Ruby/minitest requirement for CI.

Details

Also raised here

Also raised at this location by javascript: New Ruby flag tests are never run on CI (no job invokes yarn test) — The package's test script becomes ruby ios/__tests__/svg_configuration_test.rb && jest, so the new flag logic is covered only by that script — but no job in .github/workflows/ci.yml invokes yarn test (the jobs are lint (which runs yarn lint/yarn typecheck), swift-lint, build-library, build-android, build-ios, and the two jobs added here). The 6 new Ruby tests for svg_configuration.rb — the code that decides whether the private CoreSVG code is compiled — therefore never run on CI, and the PR description's "yarn test: passes, with 6 Ruby tests" reflects a local run only. Two consequences: (a) a regression in setting/xcconfig/warn_if_unset (e.g. the Podfile-wins-over-env rule or the GCC_PREPROCESSOR_DEFINITIONS string) merges silently, and (b) on any contributor machine or runner image without a ruby binary the whole test script now fails, not just the Ruby part, because && short-circuits before jest.

Also raised at this location by swift-ios: package.json test script depends on a bare ruby binary, with no CI job that runs it — "test": "ruby ios/__tests__/svg_configuration_test.rb && jest" (package.json:41) makes the whole package's test suite fail outright wherever ruby is not on PATH (sh: ruby: command not found → exit 127, and jest never runs). No workflow in .github/workflows invokes yarn test/yarn jest/turbo run test (only build/typecheck/lint do), so the six new Ruby tests the PR description counts on never run in CI either, while a contributor on Windows without Ruby gets a red yarn test for an unrelated change.

persona: javascript, swift-ios, senior-developer

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apex-sentinel-staging like

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

contents: string,
enableSVG: boolean | undefined
): string {
const lines = contents.split('\n');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor · setPodfileSVGFlag leaves a duplicated flag when the generated block's end marker is missing

packages/react-native-bottom-tabs/src/expoPodfile.ts:15-22 removes the generated block only when both markers are found (if (start !== -1 && end !== -1)). If the Podfile's generated block end line is deleted or altered (a user tidying the file, a merge conflict resolved badly, or a previous prebuild whose end-marker line got a trailing suffix), start is found and end is -1, nothing is spliced out, and lines 27-33 then insert a second # @generated begin ... / $RNBottomTabsEnableSVG = <value> / end block. Result: the Podfile ends up with two $RNBottomTabsEnableSVG assignments, and — because the insertion point is platformLine + 1 while the orphan block sits elsewhere — the one that actually wins is whichever appears last in the file, not the one the plugin just wrote. enableSVG: true in app.json can therefore be silently overridden by a stale false (or vice versa) with no error. Nothing in src/__tests__/expoPodfile.test.ts covers the start-without-end case. Handling the orphan (drop everything from start to the next marker or to end-of-block, or remove any stray $RNBottomTabsEnableSVG line before inserting) makes the write idempotent regardless of file state.

Suggested change
const lines = contents.split('\n');
const lines = contents.split('\n');
const start = lines.findIndex((line) => line.startsWith(BLOCK_START));
const end = lines.findIndex(
(line, index) => index > start && line.startsWith(BLOCK_END)
);
if (start !== -1) {
lines.splice(start, (end === -1 ? lines.length : end + 1) - start);
}

Suggested change

Make the removal branch handle a start marker without an end marker (and add a test for it), so a hand-edited Podfile cannot end up with two conflicting $RNBottomTabsEnableSVG assignments whose winner depends on line order.

Details

persona: senior-developer, javascript

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apex-sentinel-staging like

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


@synthesize callableJSModules = _callableJSModules;

#if RCT_DEV

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor · The dev warning is sent by invoking the JS module named "RCTLog" with method "logToConsole", but RCTLog is not a call…

The dev warning is sent by invoking the JS module named "RCTLog" with method "logToConsole", but RCTLog is not a callable JS module in React Native, so RCTCallableJSModules.invokeModule would drop the call and neither the DevTools warning nor the LogBox entry the PR description reports would appear; only NSLog would.

Details

Noted in the reviewer's candidate ledger and not refuted before the budget ran out.

persona: maintainability

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apex-sentinel-staging pivot RCTLog is a callable JS module. Libraries/Core/setUpBatchedBridge.js registers it with registerModule('RCTLog', ...). I tested this with Metro: the warning shows in the React Native DevTools console.

withMaterial3Theme,
'react-native-bottom-tabs'
);
const withSVGSupport: ConfigPlugin<ConfigProps> = (config, options) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor · Prebuild deletes a hand-set $RNBottomTabsEnableSVG block for consumers who leave enableSVG unset

withSVGSupport unconditionally rewrites the app's Podfile on every expo prebuild, even when the consumer never opted in: withBottomTabs always calls withSVGSupport, and when options?.enableSVG is undefined the withPodfile mod still runs setPodfileSVGFlag(contents, undefined), which removes any existing # @generated begin react-native-bottom-tabs-svg … # @generated end block. So a user who hand-wrote the generated block (or who keeps $RNBottomTabsEnableSVG only in the Podfile and leaves the plugin option out) loses it on the next prebuild, and the build silently reverts to "flag unset" — SVG enabled plus the deprecation warning — instead of the choice they encoded. Note also that the module-level default is what makes this happen: the mod is registered unconditionally rather than only when enableSVG !== undefined, so the plugin now dirties a file it was previously never given write access to for every existing consumer.

Suggested change
const withSVGSupport: ConfigPlugin<ConfigProps> = (config, options) => {
const withSVGSupport: ConfigPlugin<ConfigProps> = (config, options) => {
if (options?.enableSVG === undefined) {
return config;
}
return withPodfile(config, (podfileConfig) => {

Suggested change

Skip the withPodfile mod entirely when options?.enableSVG === undefined (return config unchanged) so the plugin neither registers a Podfile writer nor deletes a flag block the user added by hand; document that the removal path is enableSVG: true/false only.

Details

persona: javascript

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apex-sentinel-staging note This is on purpose. The plugin generates this block and marks it "DO NOT MODIFY". If the user removes enableSVG from app.json, the plugin must remove the block, or a stale value stays. To set the flag by hand, you add $RNBottomTabsEnableSVG outside the generated block, and the plugin does not touch that line.

],
publicHeadersPath: "ios/include",
cxxSettings: [
cSettings: svgDefines.filter(\.enabled).map { .define($0.name, to: "1") },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit · Duplicated filter(\.enabled).map define expression and diverging whitespace handling between Package.swift and svg_configuration.rb

svgDefines.filter(\.enabled).map { .define($0.name, to: "1") } is written out twice (lines 83 and 84) for cSettings and cxxSettings; a single let svgSettings = svgDefines.filter(\.enabled).map { .define($0.name, to: "1") } bound above let package removes the duplication and the risk of the two lists diverging. Related: the Ruby reader normalizes with value.to_s.strip.downcase while the Swift one uses .filter { !$0.isWhitespace }.lowercased(), which also deletes inner whitespace — the two readers keep drifting apart because neither derives from the other.

Suggested change
cSettings: svgDefines.filter(\.enabled).map { .define($0.name, to: "1") },
cSettings: svgSettings,
cxxSettings: svgSettings + [
Details

Also raised here

Also raised at this location by maintainability: The svgDefines -> defines mapping is duplicated across cSettings and cxxSettings — Compute it once into a let above let package and reuse it, so the two lists cannot drift when a third define is added.

persona: javascript, maintainability

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apex-sentinel-staging like

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


import PackageDescription

// The SVG image decoder uses private CoreSVG APIs. RN_BOTTOM_TABS_ENABLE_SVG controls it

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit · Package.swift comment documents a different set of accepted values than the code it sits above

The new comment lists unset / 1 / true / 0 / false, but enableSVG also treats "no" as disabled, matching FALSE_VALUES in svg_configuration.rb whose own comment table lists its values. This is a third hand-copied list of accepted values that already disagrees with the code, and the next value added to TRUE_VALUES/FALSE_VALUES has to be remembered in all three places. Document "no" (and "yes" on the true side) or drop it, so the SwiftPM table matches the Ruby one.

Suggested change
// The SVG image decoder uses private CoreSVG APIs. RN_BOTTOM_TABS_ENABLE_SVG controls it
// The SVG image decoder uses private CoreSVG APIs. RN_BOTTOM_TABS_ENABLE_SVG controls it
// when SwiftPM resolves the package:
// unset -> enabled, with a deprecation notice in dev builds
// 1 / true / yes -> enabled
// 0 / false / no -> disabled, no private CoreSVG code is compiled
Details

persona: maintainability

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apex-sentinel-staging like

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


@implementation SvgDecoder

RCT_EXPORT_MODULE()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit · @synthesize callableJSModules and RCTCallableJSModules rely on headers SvgDecoder.mm never imports

SvgDecoder.mm adds @synthesize callableJSModules = _callableJSModules; (line 24, compiled unconditionally, not inside #if RCT_DEV) and names the RCTCallableJSModules type (line 31), but the file imports only SvgDecoder.h and <React/RCTLog.h>, and neither of the two protocols SvgDecoder declares (SvgDecoder.h:6, RCTImageDataDecoder, NativeSVGDecoderSpec) is the place that property is declared in React Native — it comes from the image-loader side. So the declaration of both the property and the class must arrive transitively through whatever RCTLog.h/RCTImageDataDecoder.h happen to pull in today; @synthesize for a property that is not declared is a hard compile error, and an RN header reshuffle (RN has moved RCTCallableJSModules and the image-loader protocols more than once) breaks both Debug and Release builds of every consumer. I could not check the RN headers in this offline checkout, hence the lowered confidence; the same applies to RCT_DEV, which is only correct if RCTDefines.h really is imported by RCTLog.h on the supported RN versions. The disabled/RCT_DEV code paths are also the ones no CI job compiles as Release-with-dev-warnings, so a regression here surfaces only for a subset of consumers.

Suggested change
RCT_EXPORT_MODULE()
#import "SvgDecoder.h"
#import <React/RCTLog.h>
#import <React/RCTCallableJSModules.h>
#import <React/RCTImageLoaderProtocol.h>

Suggested change

Import the headers you use directly (#import <React/RCTCallableJSModules.h>, and the image-loader header that declares the callableJSModules property) rather than relying on transitive imports, and move @synthesize inside the same #if RCT_DEV guard as its only use so a release build does not depend on the property existing.

Details

persona: swift-ios

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apex-sentinel-staging pivot RCTImageDataDecoder.h imports RCTBridge.h, and the protocol is declared as RCTImageDataDecoder . RCTBridgeModule.h declares the callableJSModules property in all builds and tells modules to add @synthesize callableJSModules. SvgDecoder.h imports RCTImageDataDecoder.h, so the declaration is a direct part of the protocol that SvgDecoder adopts


@implementation SvgDecoder

RCT_EXPORT_MODULE()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit · @synthesize callableJSModules sits outside the #if RCT_DEV guard that covers its only reader

@synthesize callableJSModules = _callableJSModules; at SvgDecoder.mm:24 is emitted unconditionally, while the whole RCTImageDataDecoder protocol it satisfies and the logDevWarning: reader at line 31 are gated behind #if RCT_DEV (the RCTImageDataDecoder protocol declares @property (nonatomic, weak) RCTCallableJSModules *callableJSModules;). If that declaration turns out to be dev-only in a given React version (as the surrounding #if RCT_DEV blocks around the call sites assume), the synthesized accessor for an undeclared property is a hard error in the SVG-disabled Release build — exactly the configuration this PR adds a CI job for, and the reason @synthesize normally sits inside the same #if RCT_DEV guard as its only user. Either way it is inconsistent: the only consumer is dev-only, so the ivar/accessor are dead weight (and an unused-ivar/property warning surface) in every non-dev build.

Suggested change
RCT_EXPORT_MODULE()
#if RCT_DEV
@synthesize callableJSModules = _callableJSModules;
// RCTLogWarn alone does not show the message in React Native DevTools, so the

Suggested change

Move @synthesize callableJSModules = _callableJSModules; inside the existing #if RCT_DEV block that contains logDevWarning: (verifying whether RCTImageDataDecoder declares the property unconditionally), so the synthesized accessor is compiled exactly when something reads it.

Details

persona: javascript

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apex-sentinel-staging pivot The property comes from RCTBridgeModule and is declared in all builds, not only in dev builds. Because SvgDecoder adopts the protocol, it must synthesize the property in Release too. If it does not, the compiler warns about a missing property implementation.


@synthesize callableJSModules = _callableJSModules;

#if RCT_DEV

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit · logDevWarning silently drops the deprecation warning when callableJSModules is nil in a Release build

logDevWarning: is compiled only under #if RCT_DEV (line 26), but it reads self.callableJSModules, which RCTImageLoader only injects during its own (dev-only) initialization. In the CI job added by this PR the SwiftPM example is built with -configuration Release, where RCTImageLoader's #if RCT_DEV setup does not run, so callableJSModules stays nil and the else branch emits a plain RCTLogWarn — which is itself compiled out when RCT_DEV is 0. The result: the deprecation/disabled warning the PR describes as "logged to the JavaScript console" never reaches any consumer of a Release-with-dev-logging build; the same asymmetry applies to any app whose image decoding happens before the loader wires the decoder. At minimum the fallback path is dead code under the very configuration it is written for.

Details

persona: javascript

Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.

Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apex-sentinel-staging pivot logDevWarning: and all of its callers are compiled only when RCT_DEV is set. In a Release build there is no dev warning to lose, and the pod install warning covers that case. In dev builds, React Native sets callableJSModules on the module, and the DevTools test showed the warning.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: allow opting out of the private-CoreSVG SVG decoder (exclude_files by default, or an opt-in flag)

2 participants