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: