diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv
index 3baff5a5f..0d925df8c 100644
--- a/LICENSE-3rdparty.csv
+++ b/LICENSE-3rdparty.csv
@@ -3,6 +3,7 @@ prod,big-integer,Unlicense,"Free and unencumbered software released into the pub
prod,lodash.isequal,CC0,"Copyright jQuery Foundation and other contributors "
prod,react-native,MIT,"Copyright (c) Facebook, Inc. and its affiliates."
dev,@apollo/client,MIT,"Copyright (c) 2022 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.)"
+dev,babel-plugin-module-resolver,MIT,"Copyright (c) 2015 Tommy Leunen (tommyleunen.com)"
dev,@babel/plugin-transform-runtime,MIT,"Copyright (c) 2014-present Sebastian McKenzie and other contributors"
dev,@testing-library/react-native,MIT,"Copyright (c) 2018 Callstack and Rally Health"
dev,@types/jest,MIT,"Copyrights are respective of each contributor listed at the beginning of each definition file."
@@ -37,4 +38,5 @@ prod,@openfeature/web-sdk,Apache-2.0,"Copyright (c) The OpenFeature Authors"
prod,chokidar,MIT,"Copyright (c) 2012 Paul Miller (https://paulmillr.com), Elan Shanker"
prod,fast-glob,MIT,"Copyright (c) Denis Malinochkin"
prod,svgo,MIT,"Copyright (c) Kir Belevich"
+prod,tsconfig-paths,MIT,"Copyright (c) 2016 Jonas Kello"
prod,uuid,MIT,"Copyright (c) 2010-2020 Robert Kieffer and other contributors"
diff --git a/benchmarks/babel.config.js b/benchmarks/babel.config.js
index d79333729..267288b75 100644
--- a/benchmarks/babel.config.js
+++ b/benchmarks/babel.config.js
@@ -1,6 +1,33 @@
+const path = require('path');
+
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
+ // Used by Group H in the SVG test screen to verify aliased imports
+ // (e.g. '@assets/star.svg') resolve both at runtime (via this plugin
+ // rewriting the import) and in buildSvgMap's static scan (RUM-12185).
+ ['module-resolver', {
+ root: ['./src'],
+ alias: {
+ '@assets': './src/scenario/SessionReplay/component/assets',
+ // H2/H3 alias into @react-native/debugger-frontend (a real,
+ // non-workspace npm dependency -- NOT a yarn-workspace symlink,
+ // so its file path genuinely contains 'node_modules'). This matters
+ // because the Babel plugin explicitly skips any file under
+ // node_modules (index.ts), so RNSvgHandler never independently
+ // wraps these icons' own tag the way it does for in-project
+ // assets/*.svg -- localSvgMap/pathAliasResolver is the ONLY thing
+ // that can make these show up wrapped in Session Replay, which is
+ // what actually exercises the aliasing fix (RUM-12185).
+ //
+ // H2: alias maps straight to a file -- the specifier itself carries
+ // no '.svg' extension, exercising buildSvgMap's extensionless-alias path.
+ '@heart-logo': './node_modules/@react-native/debugger-frontend/dist/third-party/front_end/Images/checkmark.svg',
+ // H3: alias substitute is an absolute path rather than one relative to
+ // the importing file, exercising buildSvgMap's absolute-alias path.
+ '@absoluteAssets': path.resolve(__dirname, 'node_modules/@react-native/debugger-frontend/dist/third-party/front_end/Images')
+ }
+ }],
['@datadog/mobile-react-native-babel-plugin', {
sessionReplay: {
svgTracking: true
diff --git a/benchmarks/ios/Podfile.lock b/benchmarks/ios/Podfile.lock
index b1f782042..a51817808 100644
--- a/benchmarks/ios/Podfile.lock
+++ b/benchmarks/ios/Podfile.lock
@@ -2140,9 +2140,9 @@ SPEC CHECKSUMS:
DatadogInternal: e5a4652ee266986e29c6b8491aff7f78dc3b731d
DatadogLogs: 52792add5827042f619771a23656e2bfb6b0f4e7
DatadogRUM: b78d608e013f39c5679ed31ad97dad323c6113e8
- DatadogSDKReactNative: c41dff3ceb2b6951785bebfb43db8328040230cc
- DatadogSDKReactNativeSessionReplay: 928aa22a4e4a5927cd9fadef0fe423181500aa73
- DatadogSDKReactNativeWebView: 4c4e98715b04be591b63558d035a18ac24bfb1f9
+ DatadogSDKReactNative: 5f1ebb9df673f14f3e25a2916ef7aa330e3356ad
+ DatadogSDKReactNativeSessionReplay: 254e953f01ee0475f4f6a3fd8ccb0377690ab8ed
+ DatadogSDKReactNativeWebView: 7ca49bd5314563807755fe0db9c447994b2138d9
DatadogSessionReplay: 932b2077b1a5367de1450479d72093621b9fd618
DatadogTrace: 5ef84e254e2e1d337bc52e0cdf16396ca102aeb1
DatadogWebViewTracking: e0470157f55a83680a549d79ba2985e0d17465db
@@ -2161,68 +2161,68 @@ SPEC CHECKSUMS:
RCTTypeSafety: 48ad3c858926b1c46f46a81a58822b476e178e2c
React: 3b5754191f1b65f1dbc52fbea7959c3d2d9e39c9
React-callinvoker: 6beeaf4c7db11b6cc953fac45f2c76e3fb125013
- React-Core: 88e817c42de035378cc71e009193b9a044d3f595
- React-CoreModules: dcf764d71efb4f75d38fcae8d4513b6729f49360
- React-cxxreact: 8cdcc937c5fbc406fe843a381102fd69440ca78a
+ React-Core: 8a10ac9de53373a3ecb5dfcbcf56df1d3dad0861
+ React-CoreModules: af6999b35c7c01b0e12b59d27f3e054e13da43b1
+ React-cxxreact: 833f00155ce8c2fda17f6d286f8eaeff2ececc69
React-debug: 440175830c448e7e53e61ebb8d8468c3256b645e
- React-defaultsnativemodule: 4824bcd7b96ee2d75c28b1ca21f58976867f5535
- React-domnativemodule: a421118b475618961cf282e8ea85347cc9bb453c
- React-Fabric: 6ac7de06009eb96b609a770b17abba6e460b5f45
- React-FabricComponents: e3bc2680a5a9a4917ff0c8d7f390688c30ef753c
- React-FabricImage: 8bad558dec7478077974caa96acc79692d6b71f5
+ React-defaultsnativemodule: a970effe18fe50bdbbb7115c3297f873b666d0d4
+ React-domnativemodule: 45f886342a724e61531b18fba1859bb6782e5d62
+ React-Fabric: 69f1881f2177a8512304a64157943548ab6df0cf
+ React-FabricComponents: f54111c8e2439fc273ab07483e3a7054ca1e75af
+ React-FabricImage: 9ad2619dfe8c386d79e8aaa87da6e8f018ab9592
React-featureflags: b9cf9b35baca1c7f20c06a104ffc325a02752faa
- React-featureflagsnativemodule: dc93d81da9f41f7132e24455ec8b4b60802fd5b0
- React-graphics: aaa5a38bea15d7b895b210d95d554af45a07002a
- React-hermes: 08ad9fb832d1b9faef391be17309aa6a69fad23b
- React-idlecallbacksnativemodule: aacea33ef6c511a9781f9286cc7cdf93f39bba14
- React-ImageManager: c596c3b658c9c14607f9183ed0f635c8dd77987c
- React-jserrorhandler: 987609b2f16b7d79d63fcd621bf0110dd7400b35
- React-jsi: afa286d7e0c102c2478dc420d4f8935e13c973fc
- React-jsiexecutor: 08f5b512b4db9e2f147416d60a0a797576b9cfef
- React-jsinspector: 5a94bcae66e3637711c4d96a00038ab9ec935bf5
- React-jsinspectortracing: a12589a0adbb2703cbc4380dabe9a58800810923
- React-jsitracing: 0b1a403d7757cec66b7dd8b308d04db85eef75f3
- React-logger: 304814ae37503c8eb54359851cc55bd4f936b39c
- React-Mapbuffer: b588d1ca18d2ce626f868f04ab12d8b1f004f12c
- React-microtasksnativemodule: 11831d070aa47755bb5739069eb04ec621fec548
- react-native-config: 3367df9c1f25bb96197007ec531c7087ed4554c3
- react-native-safe-area-context: 9b169299f9dc95f1d7fe1dd266fde53bd899cd0c
- react-native-slider: 27263d134d55db948a4706f1e47d0ec88fb354dd
- react-native-webview: be9957759cb73cb64f2ed5359e32a85f1f5bdff8
- React-NativeModulesApple: 79a4404ac301b40bec3b367879c5e9a9ce81683c
- React-perflogger: 0ea25c109dba33d47dec36b2634bf7ea67c1a555
- React-performancetimeline: f74480de6efbcd8541c34317c0baedb433f27296
+ React-featureflagsnativemodule: 7f1bc76d1d2c5bede5e753b8d188dbde7c59b12f
+ React-graphics: 069e0d0b31ed1e80feb023ad4f7e97f00e84f7b9
+ React-hermes: 63df5ac5a944889c8758a6213b39ed825863adb7
+ React-idlecallbacksnativemodule: 4c700bd7c0012adf904929075a79418b828b5ffc
+ React-ImageManager: 5d1ba8a7bae44ebba43fc93da64937c713d42941
+ React-jserrorhandler: 0defd58f8bb797cdd0a820f733bf42d8bee708ce
+ React-jsi: 99d6207ec802ad73473a0dad3c9ad48cd98463f6
+ React-jsiexecutor: 8c8097b4ba7e7f480582d6e6238b01be5dcc01c0
+ React-jsinspector: ea148ec45bc7ff830e443383ea715f9780c15934
+ React-jsinspectortracing: 46bb2841982f01e7b63eaab98140fa1de5b2a1db
+ React-jsitracing: c1063fc2233960d1c8322291e74bca51d25c10d7
+ React-logger: 763728cf4eebc9c5dc9bfc3649e22295784f69f3
+ React-Mapbuffer: 63278529b5cf531a7eaf8fc71244fabb062ca90c
+ React-microtasksnativemodule: 6a39463c32ce831c4c2aa8469273114d894b6be9
+ react-native-config: 644074ab88db883fcfaa584f03520ec29589d7df
+ react-native-safe-area-context: afcc2e2b3e78ae8ef90d81e658aacee34ebc27ea
+ react-native-slider: 310d3f89edd6ca8344a974bfe83a29a3fbb60e5a
+ react-native-webview: 80ef603d1df42e24fdde765686fbb9b8a6ecd554
+ React-NativeModulesApple: fd0545efbb7f936f78edd15a6564a72d2c34bb32
+ React-perflogger: 5f8fa36a8e168fb355efe72099efe77213bc2ac6
+ React-performancetimeline: 8c0ecfa1ae459cc5678a65f95ac3bf85644d6feb
React-RCTActionSheet: 2ef95837e89b9b154f13cd8401f9054fc3076aff
- React-RCTAnimation: 33d960d7f58a81779eea6dea47ad0364c67e1517
- React-RCTAppDelegate: 85c13403fd6f6b6cc630428d52bd8bd76a670dc9
- React-RCTBlob: 74c986a02d951931d2f6ed0e07ed5a7eb385bfc0
- React-RCTFabric: 384a8fea4f22fc0f21299d771971862883ba630a
- React-RCTFBReactNativeSpec: eb1c3ec5149f76133593a516ff9d5efe32ebcecd
- React-RCTImage: 2c58b5ddeb3c65e52f942bbe13ff9c59bd649b09
- React-RCTLinking: b6b14f8a3e62c02fc627ac4f3fb0c7bd941f907c
- React-RCTNetwork: 1d050f2466c1541b339587d46f78d5eee218d626
- React-RCTSettings: 8148f6be0ccc0cfe6e313417ebf8a479caaa2146
- React-RCTText: 64114531ad1359e4e02a4a8af60df606dbbabc25
- React-RCTVibration: f4859417a7dd859b6bf18b1aba897e52beb72ef6
+ React-RCTAnimation: 46abefd5acfda7e6629f9e153646deecc70babd2
+ React-RCTAppDelegate: 7e58e0299e304cceee3f7019fa77bc6990f66b22
+ React-RCTBlob: f68c63a801ef1d27e83c4011e3b083cc86a200d7
+ React-RCTFabric: c59f41d0c4edbaac8baa232731ca09925ae4dda7
+ React-RCTFBReactNativeSpec: 3240b9b8d792aa4be0fb85c9898fc183125ba8de
+ React-RCTImage: 34e0bba1507e55f1c614bd759eb91d9be48c8c5b
+ React-RCTLinking: a0b6c9f4871c18b0b81ea952f43e752718bd5f1d
+ React-RCTNetwork: bdafd661ac2b20d23b779e45bf7ac3e4c8bd1b60
+ React-RCTSettings: 98aa5163796f43789314787b584a84eba47787a9
+ React-RCTText: 424a274fc9015b29de89cf3cbcdf4dd85dd69f83
+ React-RCTVibration: 92d9875a955b0adb34b4b773528fdbbbc5addd6c
React-rendererconsistency: 5ac4164ec18cfdd76ed5f864dbfdc56a5a948bc9
- React-rendererdebug: 3dc1d97bbee0c0c13191e501a96ed9325bbd920e
+ React-rendererdebug: 710dbd7990e355852c786aa6bc7753f6028f357a
React-rncore: 0bace3b991d8843bb5b57c5f2301ec6e9c94718b
- React-RuntimeApple: 1e1e0a0c6086bc8c3b07e8f1a2f6ca99b50419a0
- React-RuntimeCore: d39322c59bef2a4b343fda663d20649f29f57fcc
+ React-RuntimeApple: 701ec44a8b5d863ee9b6a2b2447b6a26bb6805a1
+ React-RuntimeCore: a82767065b9a936b05e209dc6987bc1ea9eb5d2d
React-runtimeexecutor: 876dfc1d8daa819dfd039c40f78f277c5a3e66a6
- React-RuntimeHermes: 44f5f2baf039f249b31ea4f3e224484fd1731e0e
- React-runtimescheduler: 3b3c5b50743bb8743ca49b9e5a70c2c385f156e1
+ React-RuntimeHermes: e7a051fd91cab8849df56ac917022ef6064ad621
+ React-runtimescheduler: c544141f2124ee3d5f3d5bf0d69f4029a61a68b0
React-timing: 1ee3572c398f5579c9df5bf76aacddf5683ff74e
- React-utils: 0cfb7c7fb37d4e5f31cc18ffc7426be0ae6bf907
- ReactAppDependencyProvider: b48473fe434569ff8f6cb6ed4421217ebcbda878
- ReactCodegen: 653a0d8532d8c7dab50c391392044d98e20c9f79
- ReactCommon: 547db015202a80a5b3e7e041586ea54c4a087180
- RNCPicker: ffbd7b9fc7c1341929e61dbef6219f7860f57418
- RNScreens: 0f01bbed9bd8045a8d58e4b46993c28c7f498f3c
- RNSVG: 082ae2874b288a96c95326c4f6029793f7009d4b
+ React-utils: 18703928768cb37e70cf2efff09def12d74a399e
+ ReactAppDependencyProvider: 4893bde33952f997a323eb1a1ee87a72764018ff
+ ReactCodegen: da30aff1cea9b5993dcbc33bf1ef47a463c55194
+ ReactCommon: 865ebe76504a95e115b6229dd00a31e56d2d4bfe
+ RNCPicker: cfb51a08c6e10357d9a65832e791825b0747b483
+ RNScreens: 790123c4a28783d80a342ce42e8c7381bed62db1
+ RNSVG: 8ad5c94593e5e4a6e133e6934e2188e3c688d27a
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: e14bad835e12b6c7e2260fc320bd00e0f4b45add
PODFILE CHECKSUM: d0574c1c0410627561bf4b1b55d0f1754283e022
-COCOAPODS: 1.16.2
+COCOAPODS: 1.17.0
diff --git a/benchmarks/metro.config.js b/benchmarks/metro.config.js
index 80b2747c5..21a754986 100644
--- a/benchmarks/metro.config.js
+++ b/benchmarks/metro.config.js
@@ -47,10 +47,28 @@ const config = {
)
)
),
- extraNodeModules: modules.reduce((acc, name) => {
- acc[name] = path.join(__dirname, 'node_modules', name);
- return acc;
- }, {})
+ extraNodeModules: {
+ ...modules.reduce((acc, name) => {
+ acc[name] = path.join(__dirname, 'node_modules', name);
+ return acc;
+ }, {}),
+ // H4: alias configured directly in metro.config.js (rather than
+ // babel-plugin-module-resolver/tsconfig.json), exercising
+ // buildSvgMap's resolver.extraNodeModules alias path. Unscoped
+ // (no leading '@') so it matches for any subpath depth -- Metro's
+ // own parsing only splits a scoped key at a *second* slash, so
+ // e.g. '@metroAssets/star.svg' wouldn't match a key of just
+ // '@metroAssets' (see pathAliasResolver.ts).
+ //
+ // Points into @react-native/debugger-frontend (a real npm
+ // dependency, not a workspace symlink) for the same reason as
+ // the babel-plugin-module-resolver aliases above -- see
+ // babel.config.js.
+ metroAssets: path.join(
+ __dirname,
+ 'node_modules/@react-native/debugger-frontend/dist/third-party/front_end/Images'
+ )
+ }
},
};
diff --git a/benchmarks/package.json b/benchmarks/package.json
index 0487960da..472a97e89 100644
--- a/benchmarks/package.json
+++ b/benchmarks/package.json
@@ -52,6 +52,7 @@
"@react-native/typescript-config": "0.78.2",
"@types/jest": "29.5.13",
"@types/react-test-renderer": "19.0.0",
+ "babel-plugin-module-resolver": "5.0.2",
"eslint": "8.19.0",
"jest": "29.6.3",
"prettier": "2.8.8",
diff --git a/benchmarks/src/scenario/SessionReplay/component/Svg.tsx b/benchmarks/src/scenario/SessionReplay/component/Svg.tsx
index ae86f9950..3e44c50c8 100644
--- a/benchmarks/src/scenario/SessionReplay/component/Svg.tsx
+++ b/benchmarks/src/scenario/SessionReplay/component/Svg.tsx
@@ -23,6 +23,20 @@ import {
import StarSvg from './assets/star.svg';
import { HeartIcon, ShieldIcon } from './assets/icons';
+// Aliased via the 'module-resolver' babel plugin (see benchmarks/babel.config.js) —
+// tests that buildSvgMap resolves aliased local SVG imports (RUM-12185).
+import AliasedStarSvg from '@assets/star.svg';
+// H2/H3/H4 alias into @react-native/debugger-frontend -- a real npm
+// dependency (not a workspace symlink), so the Babel plugin's node_modules
+// exclusion applies to it, meaning localSvgMap/pathAliasResolver is the
+// only thing that can make these show up wrapped in Session Replay (see
+// babel.config.js for why this matters).
+// H2: alias resolves straight to a file -- specifier has no '.svg' extension.
+import CheckmarkLogo from '@heart-logo';
+// H3: alias substitute is an absolute filesystem path.
+import AbsoluteAliasedLock from '@absoluteAssets/lock.svg';
+// H4: alias configured in metro.config.js's resolver.extraNodeModules.
+import MetroAliasedGear from 'metroAssets/gear-filled.svg';
// Module-level const used in Case D1 to test findIdentifierInScope
const BADGE_SIZE = 72;
@@ -340,6 +354,35 @@ function BarrelShieldImport() {
return ;
}
+// ─────────────────────────────────────────────────────────────
+// GROUP H — Aliased import (RUM-12185)
+// Same star.svg as F1, but imported via the '@assets' alias configured
+// through babel-plugin-module-resolver in babel.config.js.
+// ─────────────────────────────────────────────────────────────
+
+/** H1: Default import of a local .svg file via an aliased path */
+function AliasedStarImport() {
+ return ;
+}
+
+/** H2: Alias substitute maps straight to a file, so the specifier itself
+ * ('@heart-logo') carries no '.svg' extension at all. */
+function AliasedExtensionlessImport() {
+ return ;
+}
+
+/** H3: Alias substitute is an absolute filesystem path rather than one
+ * relative to the importing file. */
+function AbsoluteAliasImport() {
+ return ;
+}
+
+/** H4: Alias configured in metro.config.js's resolver.extraNodeModules,
+ * rather than via babel-plugin-module-resolver or tsconfig.json. */
+function MetroExtraNodeModulesImport() {
+ return ;
+}
+
// ─────────────────────────────────────────────────────────────
// GROUP I — Unsupported nested elements
// AnimatedPath isn't a recognized SVG tag, so it's now spliced out of the tree
@@ -432,6 +475,7 @@ export default function SvgTestCases() {
Group E: known limitation — absent from replay entirely (see comment).{'\n'}
Group F: appears after buildSvgMap fixes.{'\n'}
Group G: privacy overrides — verify masking behavior in replay.{'\n'}
+ Group H: aliased imports — resolved via module-resolver, absolute-path, and metro.config.js aliases.{'\n'}
Group I: I1 shows circle only (checkmark removed), I2 shows circle + checkmark.
@@ -522,6 +566,21 @@ export default function SvgTestCases() {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{/* ─── GROUP G — Privacy interaction ─── */}
{/*
These cases test whether the native SDK's view-level privacy mechanism
diff --git a/packages/react-native-babel-plugin/package.json b/packages/react-native-babel-plugin/package.json
index 5c4eaff01..725698b7d 100644
--- a/packages/react-native-babel-plugin/package.json
+++ b/packages/react-native-babel-plugin/package.json
@@ -50,6 +50,7 @@
"@babel/types": "^7.27.7",
"fast-glob": "^3.3.3",
"svgo": "^4.0.2",
+ "tsconfig-paths": "^4.2.0",
"uuid": "^8.3.2"
},
"devDependencies": {
@@ -60,6 +61,7 @@
"@swc/core": "^1.13.21",
"@swc/jest": "^0.2.38",
"@types/jest": "^30.0.0",
+ "babel-plugin-module-resolver": "5.0.2",
"jest": "^29.7.0",
"react-native-builder-bob": "0.26.0",
"tsc-alias": "^1.8.16",
diff --git a/packages/react-native-babel-plugin/src/libraries/react-native-svg/index.ts b/packages/react-native-babel-plugin/src/libraries/react-native-svg/index.ts
index 72ead6ae7..0a2bb6adf 100644
--- a/packages/react-native-babel-plugin/src/libraries/react-native-svg/index.ts
+++ b/packages/react-native-babel-plugin/src/libraries/react-native-svg/index.ts
@@ -18,6 +18,7 @@ import { v4 as uuidv4 } from 'uuid';
import { getNodeName } from '../../utils';
import { HandlerResolver } from './handlers/HandlerResolver';
+import { PathAliasResolver } from './pathAliasResolver';
import { writeAssetToDisk } from './processing/fs';
// Used when the caller (e.g. the plugin's own pre() hook) doesn't have a more
@@ -32,6 +33,17 @@ const DEFAULT_SCAN_IGNORE_PATTERNS = [
'**/*.config.js'
];
+// Mirrors the early-exit check in PathAliasResolver.resolve() -- a relative
+// or absolute specifier can never resolve through it (it guarantees `null`
+// for both), so there's no point deferring one into pendingBareSources at
+// all. Relative component imports rendered as JSX (`import Foo from
+// './Foo'; `) are the single most common React import pattern, so
+// skipping this eagerly avoids a closure allocation and a second-pass
+// iteration for the overwhelming majority of a typical project's imports.
+function isBareSpecifier(source: string): boolean {
+ return source[0] !== '.' && !pathN.isAbsolute(source);
+}
+
/**
* Internal processor responsible for detecting, transforming, and wrapping
* React Native SVG components for use with Session Replay.
@@ -47,13 +59,17 @@ export class ReactNativeSVG {
t: typeof Babel.types | null = null;
+ private pathAliasResolver: PathAliasResolver;
+
constructor(
private rootDir: string,
private assetsPath: string,
private saveSvgMapToDisk: boolean = false,
private scanIgnorePatterns: string[] = DEFAULT_SCAN_IGNORE_PATTERNS,
private followSymlinks: boolean = false
- ) {}
+ ) {
+ this.pathAliasResolver = new PathAliasResolver(rootDir);
+ }
setApiTypes(t: typeof Babel.types) {
this.t = t;
@@ -100,7 +116,11 @@ export class ReactNativeSVG {
}
}
- // TODO: Support aliased paths (RUM-12185)
+ // Drop any alias config cached from a previous buildSvgMap() run --
+ // otherwise edits to tsconfig.json/babel.config.js made since then
+ // would be invisible to a reused instance.
+ this.pathAliasResolver.reset();
+
const files = glob.sync('**/*.{js,jsx,ts,tsx}', {
cwd: this.rootDir,
absolute: true,
@@ -108,6 +128,27 @@ export class ReactNativeSVG {
followSymbolicLinks: this.followSymlinks
});
+ // An extensionless aliased import (e.g. `@logo` -> a .svg with no
+ // extension in the specifier) can only be told apart from an
+ // ordinary bare import (e.g. 'react') by actually attempting alias
+ // resolution, which is expensive per file (it can trigger
+ // @babel/core's loadPartialConfig()). Running that for every bare
+ // import in every file -- most of which are never SVGs -- would
+ // regress badly on large codebases. So this defers alias resolution
+ // for non-'.svg'-suffixed sources to a second pass, run only for
+ // import/export names that are provably rendered as JSX somewhere
+ // in the project (checked project-wide, not per file, since a
+ // barrel re-export and its JSX usage can live in different files --
+ // see the barrel-export tests). An import never rendered as JSX
+ // could never be looked up via localSvgMap anyway.
+ const pendingBareSources: Array<{
+ file: string;
+ source: string;
+ candidateNames: string[];
+ populate: (resolved: string) => void;
+ }> = [];
+ const usedJsxNames = new Set();
+
for (const file of files) {
try {
const code = fs.readFileSync(file, 'utf8');
@@ -127,63 +168,119 @@ export class ReactNativeSVG {
});
traverse(ast, {
+ JSXOpeningElement: path => {
+ if (!this.t) {
+ return;
+ }
+ const name = getNodeName(this.t, path.node.name);
+ if (name) {
+ usedJsxNames.add(name);
+ }
+ },
ImportDeclaration: path => {
if (!this.t) {
return;
}
const source = path.node.source.value;
- if (!source.endsWith('.svg')) {
+
+ if (source.endsWith('.svg')) {
+ const resolved = this.resolveImportSource(
+ file,
+ source
+ );
+ for (const spec of path.node.specifiers) {
+ const name = getNodeName(
+ this.t,
+ spec.local.name
+ );
+ if (name) {
+ this.localSvgMap[name] = {
+ path: resolved
+ };
+ }
+ }
return;
}
- const resolved = pathN.resolve(
- pathN.dirname(file),
- source
- );
+ if (!isBareSpecifier(source)) {
+ return;
+ }
+
+ const candidateNames: string[] = [];
for (const spec of path.node.specifiers) {
const name = getNodeName(this.t, spec.local.name);
if (name) {
- this.localSvgMap[name] = {
- path: resolved
- };
+ candidateNames.push(name);
}
}
+ if (!candidateNames.length) {
+ return;
+ }
+
+ pendingBareSources.push({
+ file,
+ source,
+ candidateNames,
+ populate: resolved => {
+ for (const name of candidateNames) {
+ this.localSvgMap[name] = {
+ path: resolved
+ };
+ }
+ }
+ });
},
ExportNamedDeclaration: path => {
if (!this.t) {
return;
}
const source = path.node.source?.value;
- if (!source?.endsWith('.svg')) {
+ if (!source) {
+ return;
+ }
+
+ if (source.endsWith('.svg')) {
+ const resolved = this.resolveImportSource(
+ file,
+ source
+ );
+ this.populateExportedSvgNames(path, resolved);
return;
}
- const resolved = pathN.resolve(
- pathN.dirname(file),
- source
- );
+ if (!isBareSpecifier(source)) {
+ return;
+ }
+
+ const candidateNames: string[] = [];
for (const spec of path.node.specifiers) {
- if (spec.type === 'ExportSpecifier') {
- // spec.exported is the name consumers import under
- // ('default' would be wrong for `export { default as Logo }`)
- const exported = spec.exported;
- const name = getNodeName(
- this.t,
- this.t.isStringLiteral(exported)
- ? exported.value
- : exported.name
- );
- if (name) {
- this.localSvgMap[name] = {
- path: resolved
- };
- }
- } else {
- console.warn(
- `[buildSvgMap]: Unhandled export specifier type: ${spec.type}`
- );
+ if (spec.type !== 'ExportSpecifier') {
+ continue;
+ }
+ // spec.exported is the name consumers import under
+ // ('default' would be wrong for `export { default as Logo }`)
+ const exported = spec.exported;
+ const name = getNodeName(
+ this.t,
+ this.t.isStringLiteral(exported)
+ ? exported.value
+ : exported.name
+ );
+ if (name) {
+ candidateNames.push(name);
}
}
+ if (!candidateNames.length) {
+ return;
+ }
+
+ pendingBareSources.push({
+ file,
+ source,
+ candidateNames,
+ populate: resolved =>
+ this.populateExportedSvgNames(path, resolved)
+ });
}
});
} catch (err) {
@@ -191,6 +288,25 @@ export class ReactNativeSVG {
}
}
+ // Second pass: only now attempt the (potentially expensive) alias
+ // resolution, and only for sources with at least one candidate name
+ // that's actually used as JSX somewhere in the project.
+ for (const pending of pendingBareSources) {
+ if (!pending.candidateNames.some(name => usedJsxNames.has(name))) {
+ continue;
+ }
+
+ const resolved = this.resolveSvgImportSource(
+ pending.file,
+ pending.source
+ );
+ if (!resolved) {
+ continue;
+ }
+
+ pending.populate(resolved);
+ }
+
// Save the mapping to disk if requested
if (this.saveSvgMapToDisk) {
try {
@@ -211,6 +327,68 @@ export class ReactNativeSVG {
}
}
+ /** Populates `localSvgMap` for each `ExportSpecifier` on an
+ * `export { ... } from '...svg'` declaration once its source has been
+ * resolved to a real `.svg` path. */
+ private populateExportedSvgNames(
+ path: Babel.NodePath,
+ resolved: string
+ ): void {
+ if (!this.t) {
+ return;
+ }
+
+ for (const spec of path.node.specifiers) {
+ if (spec.type === 'ExportSpecifier') {
+ // spec.exported is the name consumers import under
+ // ('default' would be wrong for `export { default as Logo }`)
+ const exported = spec.exported;
+ const name = getNodeName(
+ this.t,
+ this.t.isStringLiteral(exported)
+ ? exported.value
+ : exported.name
+ );
+ if (name) {
+ this.localSvgMap[name] = {
+ path: resolved
+ };
+ }
+ } else {
+ console.warn(
+ `[buildSvgMap]: Unhandled export specifier type: ${spec.type}`
+ );
+ }
+ }
+ }
+
+ private resolveImportSource(file: string, source: string): string {
+ return (
+ this.pathAliasResolver.resolve(source, file) ??
+ pathN.resolve(pathN.dirname(file), source)
+ );
+ }
+
+ /**
+ * Returns the resolved `.svg` file path for an import/export source, or
+ * `null` if it isn't an SVG import. A source that already ends in `.svg`
+ * is resolved directly; otherwise it may still be an aliased specifier
+ * (e.g. `alias: { '@logo': './src/assets/logo.svg' }` used as
+ * `import Logo from '@logo'`) whose specifier itself carries no
+ * extension, so alias resolution is attempted before giving up.
+ */
+ private resolveSvgImportSource(
+ file: string,
+ source: string
+ ): string | null {
+ if (source.endsWith('.svg')) {
+ return this.resolveImportSource(file, source);
+ }
+
+ const aliased = this.pathAliasResolver.resolve(source, file);
+ return aliased?.endsWith('.svg') ? aliased : null;
+ }
+
/**
* Processes a JSXElement representing an SVG-based component and transforms it into
* a web-compliant SVG string with normalized attributes and extracted dimensions.
diff --git a/packages/react-native-babel-plugin/src/libraries/react-native-svg/pathAliasResolver.ts b/packages/react-native-babel-plugin/src/libraries/react-native-svg/pathAliasResolver.ts
new file mode 100644
index 000000000..7f55ef1b0
--- /dev/null
+++ b/packages/react-native-babel-plugin/src/libraries/react-native-svg/pathAliasResolver.ts
@@ -0,0 +1,403 @@
+/*
+ * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
+ * This product includes software developed at Datadog (https://www.datadoghq.com/).
+ * Copyright 2016-Present Datadog, Inc.
+ */
+
+import * as babelCore from '@babel/core';
+import fs from 'fs';
+import pathN from 'path';
+import { createMatchPath, loadConfig } from 'tsconfig-paths';
+import type { MatchPath } from 'tsconfig-paths';
+
+// @babel/core's type declarations describe `options.plugins` as the input
+// `PluginItem[]` shape, but `loadPartialConfig()` actually resolves each
+// entry to a `ConfigItem` (undocumented in @types/babel__core) exposing
+// `.file.resolved` and `.options`.
+type ResolvedConfigItem = {
+ file?: { resolved: string };
+ options?: unknown;
+ value?: unknown;
+};
+
+type ModuleResolverBinding = {
+ resolvePath: (
+ sourcePath: string,
+ currentFile: string,
+ opts: unknown
+ ) => string | null;
+ options: unknown;
+};
+
+type ModuleResolverModule = {
+ default?: unknown;
+ resolvePath?: ModuleResolverBinding['resolvePath'];
+};
+
+function isRelativePath(value: string): boolean {
+ return /^\.?\.\//.test(value);
+}
+
+/**
+ * Splits a bare import specifier into the "package name" Metro's own
+ * resolver (`metro-resolver`'s `parseBareSpecifier`) would use to look it up
+ * in `resolver.extraNodeModules`, and the remaining subpath. Scoped-looking
+ * specifiers (starting with `@`) only split after their *second* path
+ * segment -- `@scope/pkg/sub` maps package name `@scope/pkg`, but `@scope/sub`
+ * (only one slash) has no further segment to split on, so the whole
+ * specifier is the package name, exactly mirroring Metro's own behavior.
+ */
+function parseExtraNodeModulesSpecifier(
+ specifier: string
+): {
+ packageName: string;
+ subpath: string;
+} {
+ const firstSlash = specifier.indexOf('/');
+ if (specifier[0] === '@' && firstSlash !== -1) {
+ const secondSlash = specifier.indexOf('/', firstSlash + 1);
+ if (secondSlash === -1) {
+ return { packageName: specifier, subpath: '' };
+ }
+ return {
+ packageName: specifier.slice(0, secondSlash),
+ subpath: specifier.slice(secondSlash)
+ };
+ }
+ if (firstSlash === -1) {
+ return { packageName: specifier, subpath: '' };
+ }
+ return {
+ packageName: specifier.slice(0, firstSlash),
+ subpath: specifier.slice(firstSlash)
+ };
+}
+
+/**
+ * Resolves non-relative import specifiers (e.g. `@components/Logo`) against a
+ * project's `babel-plugin-module-resolver` config, its
+ * `tsconfig.json`/`jsconfig.json` `paths` mapping, and/or its
+ * `metro.config.js` `resolver.extraNodeModules` map, so aliased local SVG
+ * imports can be found on disk the same way they resolve at runtime.
+ *
+ * Callers should still fall back to plain relative resolution when this
+ * returns `null` -- that covers projects that don't use any aliasing.
+ */
+export class PathAliasResolver {
+ private rootDir: string;
+
+ private moduleResolverBindings = new Map<
+ string,
+ ModuleResolverBinding | null
+ >();
+
+ private tsMatchPath: MatchPath | null | undefined;
+
+ private metroExtraNodeModules: Record | null | undefined;
+
+ private resultCache = new Map();
+
+ constructor(rootDir: string) {
+ this.rootDir = rootDir;
+ }
+
+ /** Drops all cached config/results -- call before reusing this resolver
+ * for a fresh scan, since a stale cache would otherwise outlive edits to
+ * tsconfig.json/Babel config made after it was first computed. */
+ reset(): void {
+ this.moduleResolverBindings.clear();
+ this.tsMatchPath = undefined;
+ this.metroExtraNodeModules = undefined;
+ this.resultCache.clear();
+ }
+
+ resolve(importSource: string, currentFile: string): string | null {
+ if (importSource[0] === '.' || pathN.isAbsolute(importSource)) {
+ return null;
+ }
+
+ const cacheKey = `${currentFile}\0${importSource}`;
+ const cached = this.resultCache.get(cacheKey);
+ if (cached !== undefined) {
+ return cached;
+ }
+
+ const resolved =
+ this.resolveWithModuleResolver(importSource, currentFile) ??
+ this.resolveWithTsconfigPaths(importSource) ??
+ this.resolveWithMetroExtraNodeModules(importSource);
+ this.resultCache.set(cacheKey, resolved);
+ return resolved;
+ }
+
+ private resolveWithModuleResolver(
+ importSource: string,
+ currentFile: string
+ ): string | null {
+ const binding = this.getModuleResolverBinding(currentFile);
+ if (!binding) {
+ return null;
+ }
+
+ try {
+ // Delegate to the project's own installed babel-plugin-module-resolver
+ // instead of re-implementing its alias/root matching -- this keeps
+ // regex-keyed aliases, function-valued aliases, and glob roots working
+ // exactly as they would at real build time.
+ const resolved = binding.resolvePath(
+ importSource,
+ currentFile,
+ binding.options
+ );
+ if (!resolved) {
+ return null;
+ }
+
+ // An `alias` entry can map to an absolute path directly (e.g.
+ // `alias: { '@app': path.resolve(__dirname, 'src') }`), not just a
+ // path relative to the importing file -- return it as-is.
+ if (pathN.isAbsolute(resolved)) {
+ return resolved;
+ }
+
+ if (!isRelativePath(resolved)) {
+ return null;
+ }
+
+ return pathN.resolve(pathN.dirname(currentFile), resolved);
+ } catch (err) {
+ console.warn(
+ '[PathAliasResolver]: babel-plugin-module-resolver failed to resolve an aliased import, falling back to relative resolution',
+ err
+ );
+ return null;
+ }
+ }
+
+ private resolveWithTsconfigPaths(importSource: string): string | null {
+ const matchPath = this.getTsMatchPath();
+ if (!matchPath) {
+ return null;
+ }
+
+ return matchPath(importSource) ?? null;
+ }
+
+ private resolveWithMetroExtraNodeModules(
+ importSource: string
+ ): string | null {
+ const extraNodeModules = this.getMetroExtraNodeModules();
+ if (!extraNodeModules) {
+ return null;
+ }
+
+ const { packageName, subpath } = parseExtraNodeModulesSpecifier(
+ importSource
+ );
+ const target = extraNodeModules[packageName];
+ if (!target) {
+ return null;
+ }
+
+ return subpath ? pathN.join(target, subpath) : target;
+ }
+
+ private getMetroExtraNodeModules(): Record | null {
+ if (this.metroExtraNodeModules !== undefined) {
+ return this.metroExtraNodeModules;
+ }
+
+ try {
+ const configPath = ['metro.config.js', 'metro.config.cjs']
+ .map(name => pathN.join(this.rootDir, name))
+ .find(candidate => fs.existsSync(candidate));
+
+ if (!configPath) {
+ this.metroExtraNodeModules = null;
+ return null;
+ }
+
+ // Unlike loadPartialConfig()/loadConfig() below (which read their
+ // config files fresh from disk each call), require() caches by
+ // resolved filename -- drop any cached entry first so an edit to
+ // metro.config.js made since this was last required is picked up
+ // after reset(), instead of silently reusing a stale module.
+ delete require.cache[require.resolve(configPath)];
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, import/no-dynamic-require
+ const config = require(configPath) as {
+ resolver?: { extraNodeModules?: unknown };
+ };
+ const extraNodeModules = config?.resolver?.extraNodeModules;
+ this.metroExtraNodeModules =
+ extraNodeModules && typeof extraNodeModules === 'object'
+ ? (extraNodeModules as Record)
+ : null;
+ } catch (err) {
+ console.warn(
+ '[PathAliasResolver]: Failed to load metro.config.js, aliased SVG imports may not resolve',
+ err
+ );
+ this.metroExtraNodeModules = null;
+ }
+
+ return this.metroExtraNodeModules;
+ }
+
+ private getTsMatchPath(): MatchPath | null {
+ if (this.tsMatchPath !== undefined) {
+ return this.tsMatchPath;
+ }
+
+ try {
+ const config = loadConfig(this.rootDir);
+ if (config.resultType === 'success') {
+ this.tsMatchPath = createMatchPath(
+ config.absoluteBaseUrl,
+ config.paths
+ );
+ } else {
+ this.tsMatchPath = null;
+ }
+ } catch (err) {
+ console.warn(
+ '[PathAliasResolver]: Failed to load tsconfig.json/jsconfig.json paths, aliased SVG imports may not resolve',
+ err
+ );
+ this.tsMatchPath = null;
+ }
+
+ return this.tsMatchPath;
+ }
+
+ private getModuleResolverBinding(
+ currentFile: string
+ ): ModuleResolverBinding | null {
+ if (this.moduleResolverBindings.has(currentFile)) {
+ return this.moduleResolverBindings.get(currentFile) ?? null;
+ }
+
+ try {
+ const partialConfig = babelCore.loadPartialConfig({
+ cwd: this.rootDir,
+ filename: currentFile
+ });
+
+ const plugins = ((partialConfig?.options.plugins ??
+ []) as unknown) as ResolvedConfigItem[];
+ // Compare with normalized (forward-slash) separators -- `file.resolved`
+ // uses the OS-native separator, which is a backslash on Windows.
+ let pluginItem = plugins.find(plugin =>
+ plugin.file?.resolved
+ .replace(/\\/g, '/')
+ .includes('/babel-plugin-module-resolver/')
+ );
+
+ let resolvedPluginPath = pluginItem?.file?.resolved;
+ let moduleResolverModule: ModuleResolverModule | undefined;
+
+ // Babel omits `file.resolved` when a config passes the plugin
+ // function directly (for example `require('...')`). Resolve the
+ // project-visible module and compare its exported function by
+ // identity so this valid config form is detected too.
+ if (!pluginItem) {
+ const bindFunctionPlugin = (
+ modulePath: string,
+ candidateModule: ModuleResolverModule
+ ): boolean => {
+ const candidatePluginItem = plugins.find(
+ plugin =>
+ plugin.value === candidateModule.default ||
+ plugin.value === candidateModule
+ );
+ if (!candidatePluginItem) {
+ return false;
+ }
+
+ resolvedPluginPath = modulePath;
+ moduleResolverModule = candidateModule;
+ pluginItem = candidatePluginItem;
+ return true;
+ };
+
+ try {
+ const projectModulePath = require.resolve(
+ 'babel-plugin-module-resolver',
+ {
+ paths: [pathN.dirname(currentFile), this.rootDir]
+ }
+ );
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, import/no-dynamic-require
+ const projectModule = require(projectModulePath) as ModuleResolverModule;
+ bindFunctionPlugin(projectModulePath, projectModule);
+ } catch (err) {
+ // The config may have required an explicit module path
+ // outside rootDir, so check already-loaded modules below.
+ }
+
+ if (!pluginItem) {
+ for (const cachedModule of Object.values(require.cache)) {
+ const modulePath = cachedModule?.filename;
+ if (
+ !cachedModule ||
+ !modulePath
+ ?.replace(/\\/g, '/')
+ .includes('/babel-plugin-module-resolver/')
+ ) {
+ continue;
+ }
+
+ if (
+ bindFunctionPlugin(
+ modulePath,
+ cachedModule.exports as ModuleResolverModule
+ )
+ ) {
+ break;
+ }
+ }
+ }
+ }
+
+ const options = pluginItem?.options;
+ if (
+ !pluginItem ||
+ !resolvedPluginPath ||
+ !options ||
+ typeof options !== 'object'
+ ) {
+ this.moduleResolverBindings.set(currentFile, null);
+ return null;
+ }
+
+ // Require the project's own installed copy (via its resolved path,
+ // rather than a bundled copy of ours) so behavior matches whatever
+ // version is actually driving the project's real bundling. The
+ // path is only known at runtime, so a dynamic require is required.
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, import/no-dynamic-require
+ moduleResolverModule ??= require(resolvedPluginPath) as ModuleResolverModule;
+
+ // babel-plugin-module-resolver defaults `cwd` to `process.cwd()`
+ // when unset, which at real build time is the project root -- but
+ // isn't necessarily true for this out-of-band scan (e.g. tests,
+ // or a CLI run from elsewhere), so pin it to rootDir explicitly.
+ const optionsWithCwd =
+ 'cwd' in options ? options : { ...options, cwd: this.rootDir };
+
+ const binding = moduleResolverModule.resolvePath
+ ? {
+ resolvePath: moduleResolverModule.resolvePath,
+ options: optionsWithCwd
+ }
+ : null;
+ this.moduleResolverBindings.set(currentFile, binding);
+ return binding;
+ } catch (err) {
+ console.warn(
+ '[PathAliasResolver]: Failed to load babel-plugin-module-resolver config, aliased SVG imports may not resolve',
+ err
+ );
+ this.moduleResolverBindings.set(currentFile, null);
+ return null;
+ }
+ }
+}
diff --git a/packages/react-native-babel-plugin/test/react-native-svg.test.ts b/packages/react-native-babel-plugin/test/react-native-svg.test.ts
index a1d6a0b31..3648d58d3 100644
--- a/packages/react-native-babel-plugin/test/react-native-svg.test.ts
+++ b/packages/react-native-babel-plugin/test/react-native-svg.test.ts
@@ -16,6 +16,7 @@ import path from 'path';
import plugin from '../src/index';
import { RNSvgHandler } from '../src/libraries/react-native-svg/handlers/RNSvgHandler';
+import { PathAliasResolver } from '../src/libraries/react-native-svg/pathAliasResolver';
import { ReactNativeSVG } from '../src/libraries/react-native-svg';
/**
@@ -1262,3 +1263,635 @@ describe('ReactNativeSVG.buildSvgMap', () => {
expect(scopedInstance.localSvgMap['StarIcon']).toBeUndefined();
});
});
+
+describe('ReactNativeSVG.buildSvgMap with aliased paths', () => {
+ let tmpDir: string;
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), 'dd-buildsvgmap-alias-')
+ );
+ fs.mkdirSync(path.join(tmpDir, 'src', 'components'), {
+ recursive: true
+ });
+ fs.writeFileSync(
+ path.join(tmpDir, 'src', 'components', 'icon.svg'),
+ ' '
+ );
+ });
+
+ afterEach(() => {
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ });
+
+ it('should resolve an aliased import using tsconfig.json baseUrl/paths', () => {
+ fs.writeFileSync(
+ path.join(tmpDir, 'tsconfig.json'),
+ JSON.stringify({
+ compilerOptions: {
+ baseUrl: '.',
+ paths: { '@components/*': ['src/components/*'] }
+ }
+ })
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from '@components/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo']).toBeDefined();
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+ });
+
+ it('should resolve an aliased import using jsconfig.json baseUrl/paths', () => {
+ fs.writeFileSync(
+ path.join(tmpDir, 'jsconfig.json'),
+ JSON.stringify({
+ compilerOptions: {
+ baseUrl: '.',
+ paths: { '@components/*': ['src/components/*'] }
+ }
+ })
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.jsx'),
+ `import Logo from '@components/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo']).toBeDefined();
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+ });
+
+ it('should resolve an aliased import using a tsconfig.json that extends a base config', () => {
+ fs.writeFileSync(
+ path.join(tmpDir, 'base.tsconfig.json'),
+ JSON.stringify({
+ compilerOptions: {
+ baseUrl: '.',
+ paths: { '@components/*': ['src/components/*'] }
+ }
+ })
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'tsconfig.json'),
+ JSON.stringify({ extends: './base.tsconfig.json' })
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from '@components/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo']).toBeDefined();
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+ });
+
+ it('should resolve an aliased import using babel-plugin-module-resolver config', () => {
+ const moduleResolverPath = require.resolve(
+ 'babel-plugin-module-resolver'
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'babel.config.js'),
+ `module.exports = {
+ plugins: [
+ [${JSON.stringify(moduleResolverPath)}, {
+ root: ['./src'],
+ alias: { '@components': './src/components' }
+ }]
+ ]
+ };`
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from '@components/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo']).toBeDefined();
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+ });
+
+ it('should resolve an aliased import using a .babelrc config', () => {
+ const moduleResolverPath = require.resolve(
+ 'babel-plugin-module-resolver'
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, '.babelrc'),
+ JSON.stringify({
+ plugins: [
+ [
+ moduleResolverPath,
+ {
+ alias: {
+ '@components': './src/components'
+ }
+ }
+ ]
+ ]
+ })
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from '@components/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+ });
+
+ it('should resolve an aliased import when module-resolver is passed as a function', () => {
+ const moduleResolverPath = require.resolve(
+ 'babel-plugin-module-resolver'
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'babel.config.js'),
+ `const moduleResolver = require(${JSON.stringify(
+ moduleResolverPath
+ )});
+ module.exports = {
+ plugins: [
+ [moduleResolver, {
+ alias: { '@components': './src/components' }
+ }]
+ ]
+ };`
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from '@components/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+ });
+
+ it('should prefer babel-plugin-module-resolver over tsconfig.json when both are configured', () => {
+ fs.mkdirSync(path.join(tmpDir, 'alt-components'));
+ fs.writeFileSync(
+ path.join(tmpDir, 'alt-components', 'icon.svg'),
+ ' '
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'tsconfig.json'),
+ JSON.stringify({
+ compilerOptions: {
+ baseUrl: '.',
+ paths: { '@components/*': ['alt-components/*'] }
+ }
+ })
+ );
+ const moduleResolverPath = require.resolve(
+ 'babel-plugin-module-resolver'
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'babel.config.js'),
+ `module.exports = {
+ plugins: [
+ [${JSON.stringify(moduleResolverPath)}, {
+ alias: { '@components': './src/components' }
+ }]
+ ]
+ };`
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from '@components/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+ });
+
+ it('should leave non-relative imports unresolved (falling back to the previous behavior) when no alias config is present', () => {
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from '@components/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ expect(() => instance.buildSvgMap()).not.toThrow();
+
+ expect(instance.localSvgMap['Logo']).toBeDefined();
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.resolve(tmpDir, '@components/icon.svg')
+ );
+ });
+
+ it('should fall back to unresolved relative resolution when an alias is configured but does not match a file on disk', () => {
+ fs.writeFileSync(
+ path.join(tmpDir, 'tsconfig.json'),
+ JSON.stringify({
+ compilerOptions: {
+ baseUrl: '.',
+ paths: { '@missing/*': ['src/does-not-exist/*'] }
+ }
+ })
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from '@missing/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ expect(() => instance.buildSvgMap()).not.toThrow();
+
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.resolve(tmpDir, '@missing/icon.svg')
+ );
+ });
+
+ it('should still resolve unaliased relative imports normally when alias config is present', () => {
+ fs.writeFileSync(
+ path.join(tmpDir, 'tsconfig.json'),
+ JSON.stringify({
+ compilerOptions: {
+ baseUrl: '.',
+ paths: { '@components/*': ['src/components/*'] }
+ }
+ })
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from './src/components/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+ });
+
+ it('should resolve an aliased import whose specifier has no .svg extension (alias points directly at the file)', () => {
+ const moduleResolverPath = require.resolve(
+ 'babel-plugin-module-resolver'
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'babel.config.js'),
+ `module.exports = {
+ plugins: [
+ [${JSON.stringify(moduleResolverPath)}, {
+ alias: { '@logo': './src/components/icon.svg' }
+ }]
+ ]
+ };`
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from '@logo';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo']).toBeDefined();
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+ });
+
+ it('should resolve an extensionless aliased re-export even when the JSX usage is in a different file (performance guard checks JSX usage project-wide, not per file)', () => {
+ const moduleResolverPath = require.resolve(
+ 'babel-plugin-module-resolver'
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'babel.config.js'),
+ `module.exports = {
+ plugins: [
+ [${JSON.stringify(moduleResolverPath)}, {
+ alias: { '@logo': './src/components/icon.svg' }
+ }]
+ ]
+ };`
+ );
+ // The extensionless-aliased import lives in a barrel file with no
+ // JSX at all -- only Screen.tsx (a separate file) ever renders it.
+ fs.writeFileSync(
+ path.join(tmpDir, 'icons.ts'),
+ `export { default as Logo } from '@logo';`
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Screen.tsx'),
+ `import { Logo } from './icons';\nexport default function Screen() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo']).toBeDefined();
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+ });
+
+ it('should NOT populate localSvgMap for an extensionless aliased import that is never rendered as JSX anywhere in the project (performance guard skips alias resolution)', () => {
+ const moduleResolverPath = require.resolve(
+ 'babel-plugin-module-resolver'
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'babel.config.js'),
+ `module.exports = {
+ plugins: [
+ [${JSON.stringify(moduleResolverPath)}, {
+ alias: { '@logo': './src/components/icon.svg' }
+ }]
+ ]
+ };`
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import UnusedLogo from '@logo';\nexport default function C() { return null; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['UnusedLogo']).toBeUndefined();
+ });
+
+ it('should never attempt alias resolution for a relative or absolute import, even when its name is rendered as JSX (they can never resolve via PathAliasResolver, so deferring them would be pure overhead)', () => {
+ const resolveSpy = jest.spyOn(PathAliasResolver.prototype, 'resolve');
+
+ fs.writeFileSync(
+ path.join(tmpDir, 'Icon.tsx'),
+ `export default function Icon() { return null; }`
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Icon from './Icon';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(resolveSpy).not.toHaveBeenCalled();
+ expect(instance.localSvgMap['Icon']).toBeUndefined();
+
+ resolveSpy.mockRestore();
+ });
+
+ it('should resolve an alias that maps directly to an absolute path', () => {
+ const moduleResolverPath = require.resolve(
+ 'babel-plugin-module-resolver'
+ );
+ const absoluteIconPath = path.join(
+ tmpDir,
+ 'src',
+ 'components',
+ 'icon.svg'
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'babel.config.js'),
+ `module.exports = {
+ plugins: [
+ [${JSON.stringify(moduleResolverPath)}, {
+ alias: { '@logo': ${JSON.stringify(absoluteIconPath)} }
+ }]
+ ]
+ };`
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from '@logo';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo']).toBeDefined();
+ expect(instance.localSvgMap['Logo'].path).toBe(absoluteIconPath);
+ });
+
+ it('should resolve an aliased import using metro.config.js resolver.extraNodeModules', () => {
+ fs.writeFileSync(
+ path.join(tmpDir, 'metro.config.js'),
+ `module.exports = {
+ resolver: {
+ extraNodeModules: {
+ assets: ${JSON.stringify(
+ path.join(tmpDir, 'src', 'components')
+ )}
+ }
+ }
+ };`
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from 'assets/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo']).toBeDefined();
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+ });
+
+ it("should resolve a scoped extraNodeModules alias only when the key matches the full specifier (single subpath segment), mirroring Metro's own parsing", () => {
+ fs.writeFileSync(
+ path.join(tmpDir, 'metro.config.js'),
+ `module.exports = {
+ resolver: {
+ extraNodeModules: {
+ '@assets/icon.svg': ${JSON.stringify(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ )}
+ }
+ }
+ };`
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from '@assets/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo']).toBeDefined();
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+ });
+
+ it("should not match a scoped extraNodeModules alias against a one-slash specifier when the key is only the scope segment, mirroring Metro's own parsing", () => {
+ fs.writeFileSync(
+ path.join(tmpDir, 'metro.config.js'),
+ `module.exports = {
+ resolver: {
+ extraNodeModules: {
+ '@assets': ${JSON.stringify(
+ path.join(tmpDir, 'src', 'components')
+ )}
+ }
+ }
+ };`
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from '@assets/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ // No extraNodeModules key matches the full '@assets/icon.svg' specifier
+ // (per Metro's own parsing, '@assets' alone isn't a match), so this
+ // falls back to unresolved relative resolution like any other
+ // unmatched alias -- same fallback as the plain non-relative-import case.
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.resolve(tmpDir, '@assets/icon.svg')
+ );
+ });
+
+ it('should prefer babel-plugin-module-resolver/tsconfig.json over metro.config.js when both are configured', () => {
+ const moduleResolverPath = require.resolve(
+ 'babel-plugin-module-resolver'
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'babel.config.js'),
+ `module.exports = {
+ plugins: [
+ [${JSON.stringify(moduleResolverPath)}, {
+ alias: { '@shared': './src/components' }
+ }]
+ ]
+ };`
+ );
+ fs.mkdirSync(path.join(tmpDir, 'other'), { recursive: true });
+ fs.writeFileSync(
+ path.join(tmpDir, 'other', 'icon.svg'),
+ ' '
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'metro.config.js'),
+ `module.exports = {
+ resolver: {
+ extraNodeModules: {
+ '@shared': ${JSON.stringify(path.join(tmpDir, 'other'))}
+ }
+ }
+ };`
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from '@shared/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+ });
+
+ it('should pick up an edit to metro.config.js resolver.extraNodeModules after reset(), rather than reusing a require()-cached module', () => {
+ fs.mkdirSync(path.join(tmpDir, 'other'), { recursive: true });
+ fs.writeFileSync(
+ path.join(tmpDir, 'other', 'icon.svg'),
+ ' '
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'metro.config.js'),
+ `module.exports = {
+ resolver: {
+ extraNodeModules: {
+ assets: ${JSON.stringify(
+ path.join(tmpDir, 'src', 'components')
+ )}
+ }
+ }
+ };`
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Component.tsx'),
+ `import Logo from 'assets/icon.svg';\nexport default function C() { return ; }`
+ );
+
+ const instance = new ReactNativeSVG(tmpDir, tmpDir, false);
+ instance.setApiTypes(t);
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'src', 'components', 'icon.svg')
+ );
+
+ // Edit metro.config.js in place (same path -- require()'s module
+ // cache is keyed by resolved filename, so this only gets picked up
+ // if the cache entry is dropped before re-requiring). jest.resetModules()
+ // clears Jest's own sandboxed module registry, which doesn't otherwise
+ // track this source's own require.cache manipulation the same way
+ // plain Node does -- without it, this test would pass regardless of
+ // whether the source actually clears require.cache itself.
+ jest.resetModules();
+ fs.writeFileSync(
+ path.join(tmpDir, 'metro.config.js'),
+ `module.exports = {
+ resolver: {
+ extraNodeModules: {
+ assets: ${JSON.stringify(path.join(tmpDir, 'other'))}
+ }
+ }
+ };`
+ );
+
+ instance.buildSvgMap();
+
+ expect(instance.localSvgMap['Logo'].path).toBe(
+ path.join(tmpDir, 'other', 'icon.svg')
+ );
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index 3deac6908..32952c181 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2039,11 +2039,13 @@ __metadata:
"@swc/core": ^1.13.21
"@swc/jest": ^0.2.38
"@types/jest": ^30.0.0
+ babel-plugin-module-resolver: 5.0.2
fast-glob: ^3.3.3
jest: ^29.7.0
react-native-builder-bob: 0.26.0
svgo: ^4.0.2
tsc-alias: ^1.8.16
+ tsconfig-paths: ^4.2.0
typescript: 5.9.3
uuid: ^8.3.2
peerDependencies:
@@ -6592,6 +6594,19 @@ __metadata:
languageName: node
linkType: hard
+"babel-plugin-module-resolver@npm:5.0.2":
+ version: 5.0.2
+ resolution: "babel-plugin-module-resolver@npm:5.0.2"
+ dependencies:
+ find-babel-config: ^2.1.1
+ glob: ^9.3.3
+ pkg-up: ^3.1.0
+ reselect: ^4.1.7
+ resolve: ^1.22.8
+ checksum: f1d198acbbbd0b76c9c0c4aacbf9f1ef90f8d36b3d5209d9e7a75cadee2113a73711550ebddeb9464d143b71df19adc75e165dff99ada2614d7ea333affe3b5a
+ languageName: node
+ linkType: hard
+
"babel-plugin-module-resolver@npm:^4.0.0":
version: 4.1.0
resolution: "babel-plugin-module-resolver@npm:4.1.0"
@@ -6800,6 +6815,7 @@ __metadata:
"@react-navigation/native-stack": 7.3.12
"@types/jest": 29.5.13
"@types/react-test-renderer": 19.0.0
+ babel-plugin-module-resolver: 5.0.2
eslint: 8.19.0
jest: 29.6.3
prettier: 2.8.8
@@ -9680,6 +9696,15 @@ __metadata:
languageName: node
linkType: hard
+"find-babel-config@npm:^2.1.1":
+ version: 2.1.2
+ resolution: "find-babel-config@npm:2.1.2"
+ dependencies:
+ json5: ^2.2.3
+ checksum: 268f29cb38ee086b0f953c89f762dcea30b5b0e14abee2b39516410c00b49baa6821f598bd50346c93584e5625c5740f5c8b7e34993f568787a068f84dacc8c2
+ languageName: node
+ linkType: hard
+
"find-cache-dir@npm:^2.0.0":
version: 2.1.0
resolution: "find-cache-dir@npm:2.1.0"
@@ -10261,6 +10286,18 @@ __metadata:
languageName: node
linkType: hard
+"glob@npm:^9.3.3":
+ version: 9.3.5
+ resolution: "glob@npm:9.3.5"
+ dependencies:
+ fs.realpath: ^1.0.0
+ minimatch: ^8.0.2
+ minipass: ^4.2.4
+ path-scurry: ^1.6.1
+ checksum: 94b093adbc591bc36b582f77927d1fb0dbf3ccc231828512b017601408be98d1fe798fc8c0b19c6f2d1a7660339c3502ce698de475e9d938ccbb69b47b647c84
+ languageName: node
+ linkType: hard
+
"global-agent@npm:^3.0.0":
version: 3.0.0
resolution: "global-agent@npm:3.0.0"
@@ -13555,6 +13592,15 @@ __metadata:
languageName: node
linkType: hard
+"minimatch@npm:^8.0.2":
+ version: 8.0.7
+ resolution: "minimatch@npm:8.0.7"
+ dependencies:
+ brace-expansion: ^2.0.1
+ checksum: edaefeb16297f4f3969287913adb04c12c5683f2bd8610c6d6bfd5aa5b98bbbfd6013a2d0bb24df62e8add9c265128df1bfdbb61bb043ef4aa86b449fc2a9c76
+ languageName: node
+ linkType: hard
+
"minimist-options@npm:4.1.0":
version: 4.1.0
resolution: "minimist-options@npm:4.1.0"
@@ -13633,6 +13679,13 @@ __metadata:
languageName: node
linkType: hard
+"minipass@npm:^4.2.4":
+ version: 4.2.8
+ resolution: "minipass@npm:4.2.8"
+ checksum: 7f4914d5295a9a30807cae5227a37a926e6d910c03f315930fde52332cf0575dfbc20295318f91f0baf0e6bb11a6f668e30cde8027dea7a11b9d159867a3c830
+ languageName: node
+ linkType: hard
+
"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2":
version: 7.1.2
resolution: "minipass@npm:7.1.2"
@@ -14894,7 +14947,7 @@ __metadata:
languageName: node
linkType: hard
-"path-scurry@npm:^1.11.1":
+"path-scurry@npm:^1.11.1, path-scurry@npm:^1.6.1":
version: 1.11.1
resolution: "path-scurry@npm:1.11.1"
dependencies:
@@ -16284,7 +16337,7 @@ __metadata:
languageName: node
linkType: hard
-"reselect@npm:^4.0.0":
+"reselect@npm:^4.0.0, reselect@npm:^4.1.7":
version: 4.1.8
resolution: "reselect@npm:4.1.8"
checksum: a4ac87cedab198769a29be92bc221c32da76cfdad6911eda67b4d3e7136dca86208c3b210e31632eae31ebd2cded18596f0dd230d3ccc9e978df22f233b5583e
@@ -16335,7 +16388,7 @@ __metadata:
languageName: node
linkType: hard
-"resolve@npm:^1.10.0, resolve@npm:^1.13.1, resolve@npm:^1.18.1, resolve@npm:^1.20.0, resolve@npm:^1.22.11, resolve@npm:^1.22.4":
+"resolve@npm:^1.10.0, resolve@npm:^1.13.1, resolve@npm:^1.18.1, resolve@npm:^1.20.0, resolve@npm:^1.22.11, resolve@npm:^1.22.4, resolve@npm:^1.22.8":
version: 1.22.12
resolution: "resolve@npm:1.22.12"
dependencies:
@@ -16362,7 +16415,7 @@ __metadata:
languageName: node
linkType: hard
-"resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.13.1#~builtin, resolve@patch:resolve@^1.18.1#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.11#~builtin, resolve@patch:resolve@^1.22.4#~builtin":
+"resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.13.1#~builtin, resolve@patch:resolve@^1.18.1#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.11#~builtin, resolve@patch:resolve@^1.22.4#~builtin, resolve@patch:resolve@^1.22.8#~builtin":
version: 1.22.12
resolution: "resolve@patch:resolve@npm%3A1.22.12#~builtin::version=1.22.12&hash=c3c19d"
dependencies:
@@ -17805,7 +17858,7 @@ __metadata:
languageName: node
linkType: hard
-"tsconfig-paths@npm:^4.1.2":
+"tsconfig-paths@npm:^4.1.2, tsconfig-paths@npm:^4.2.0":
version: 4.2.0
resolution: "tsconfig-paths@npm:4.2.0"
dependencies: