diff --git a/benchmarks/babel.config.js b/benchmarks/babel.config.js
index d79333729..d81f42512 100644
--- a/benchmarks/babel.config.js
+++ b/benchmarks/babel.config.js
@@ -1,6 +1,15 @@
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'
+ }
+ }],
['@datadog/mobile-react-native-babel-plugin', {
sessionReplay: {
svgTracking: true
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..20a4d7c2b 100644
--- a/benchmarks/src/scenario/SessionReplay/component/Svg.tsx
+++ b/benchmarks/src/scenario/SessionReplay/component/Svg.tsx
@@ -23,6 +23,9 @@ 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';
// Module-level const used in Case D1 to test findIdentifierInScope
const BADGE_SIZE = 72;
@@ -340,6 +343,17 @@ 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 ;
+}
+
// ─────────────────────────────────────────────────────────────
// GROUP I — Unsupported nested elements
// AnimatedPath isn't a recognized SVG tag, so it's now spliced out of the tree
@@ -432,6 +446,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 import — same star as F1, resolved via '@assets' alias.{'\n'}
Group I: I1 shows circle only (checkmark removed), I2 shows circle + checkmark.
@@ -522,6 +537,12 @@ 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 87aa4594c..15c6d42a8 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.1",
+ "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..d4113401e 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
@@ -47,13 +48,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 +105,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,
@@ -136,10 +145,7 @@ export class ReactNativeSVG {
return;
}
- const resolved = pathN.resolve(
- pathN.dirname(file),
- source
- );
+ const resolved = this.resolveImportSource(file, source);
for (const spec of path.node.specifiers) {
const name = getNodeName(this.t, spec.local.name);
if (name) {
@@ -158,10 +164,7 @@ export class ReactNativeSVG {
return;
}
- const resolved = pathN.resolve(
- pathN.dirname(file),
- source
- );
+ const resolved = this.resolveImportSource(file, source);
for (const spec of path.node.specifiers) {
if (spec.type === 'ExportSpecifier') {
// spec.exported is the name consumers import under
@@ -211,6 +214,13 @@ export class ReactNativeSVG {
}
}
+ private resolveImportSource(file: string, source: string): string {
+ return (
+ this.pathAliasResolver.resolve(source, file) ??
+ pathN.resolve(pathN.dirname(file), source)
+ );
+ }
+
/**
* 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..0bc823eb1
--- /dev/null
+++ b/packages/react-native-babel-plugin/src/libraries/react-native-svg/pathAliasResolver.ts
@@ -0,0 +1,291 @@
+/*
+ * 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 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);
+}
+
+/**
+ * Resolves non-relative import specifiers (e.g. `@components/Logo`) against a
+ * project's `babel-plugin-module-resolver` config and/or its
+ * `tsconfig.json`/`jsconfig.json` `paths` mapping, 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 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.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.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 || !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 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..c50f3f7b4 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
@@ -1262,3 +1262,303 @@ 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')
+ );
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index 7c8297563..880cfc30a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2781,11 +2781,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.1
tsc-alias: ^1.8.16
+ tsconfig-paths: ^4.2.0
typescript: 5.9.3
uuid: ^8.3.2
peerDependencies:
@@ -7407,6 +7409,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"
@@ -7420,6 +7435,19 @@ __metadata:
languageName: node
linkType: hard
+"babel-plugin-module-resolver@npm:^5.0.2":
+ version: 5.0.3
+ resolution: "babel-plugin-module-resolver@npm:5.0.3"
+ 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: a03a614d8a6d7eedb2f7a764503e28e41824d983c4100203c180523d4dbda8fd12d2679b8e1c769c0426595d581e818696fffb10bacf20bdb487a46acdebece3
+ languageName: node
+ linkType: hard
+
"babel-plugin-polyfill-corejs2@npm:^0.4.10":
version: 0.4.13
resolution: "babel-plugin-polyfill-corejs2@npm:0.4.13"
@@ -7639,6 +7667,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
@@ -10593,6 +10622,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"
@@ -11185,6 +11223,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"
@@ -14540,6 +14590,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"
@@ -14618,6 +14677,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"
@@ -15942,7 +16008,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:
@@ -17366,7 +17432,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
@@ -17430,7 +17496,7 @@ __metadata:
languageName: node
linkType: hard
-"resolve@npm:^1.22.11":
+"resolve@npm:^1.22.11, resolve@npm:^1.22.8":
version: 1.22.12
resolution: "resolve@npm:1.22.12"
dependencies:
@@ -17470,7 +17536,7 @@ __metadata:
languageName: node
linkType: hard
-"resolve@patch:resolve@^1.22.11#~builtin":
+"resolve@patch:resolve@^1.22.11#~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:
@@ -18913,7 +18979,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: