Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions benchmarks/src/scenario/SessionReplay/component/Svg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,16 +321,19 @@ function DynamicOpacityCircle({ fadeLevel }: { fadeLevel: number }) {

// ─────────────────────────────────────────────────────────────
// GROUP F — File imports
// LocalSvgHandler reads the SVG file from disk (Fix 2, 3, 4).
// LocalSvgHandler resolves and reads the imported SVG file from disk.
// ─────────────────────────────────────────────────────────────

/** F1: Default import of a local .svg file */
function LocalStarImport() {
return <StarSvg width={64} height={64} />;
}

/** F2: Named import from barrel file (export { default as HeartIcon })
* Tests Fix 6: ExportNamedDeclaration uses spec.exported.name, not spec.local.name */
/**
* F2: Named import from a barrel file (export { default as HeartIcon } from './heart.svg').
* The barrel scan must key this by the exported name ('HeartIcon'), not the source
* module's local name ('default') — otherwise this import is never resolved.
*/
function BarrelHeartImport() {
return <HeartIcon width={64} height={64} />;
}
Expand Down Expand Up @@ -430,7 +433,7 @@ export default function SvgTestCases() {
<RNText style={styles.subtitle}>
All cases in Groups A–D should appear in replay.{'\n'}
Group E: known limitation — absent from replay entirely (see comment).{'\n'}
Group F: appears after buildSvgMap fixes.{'\n'}
Group F: direct and barrel-re-exported .svg imports should appear in replay.{'\n'}
Group G: privacy overrides — verify masking behavior in replay.{'\n'}
Group I: I1 shows circle only (checkmark removed), I2 shows circle + checkmark.
</RNText>
Expand Down Expand Up @@ -510,7 +513,7 @@ export default function SvgTestCases() {
</Case>
</Section>

<Section title="F — File imports (Fixes 2, 3, 4, 6)">
<Section title="F — File imports (direct + barrel re-exports)">
<Case label="F1 default import" sublabel="star.svg">
<LocalStarImport />
</Case>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* Copyright 2016-Present Datadog, Inc.
*/

// Barrel file used to test Fix 6: ExportNamedDeclaration must use spec.exported.name
// not spec.local.name, otherwise 'export { default as HeartIcon }' stores 'default'
// as the key in localSvgMap instead of 'HeartIcon', and <HeartIcon/> is never found.
// Barrel file used to test aliased re-exports: the SVG scan must key this
// entry by the exported name ('HeartIcon'), not the source module's local
// name ('default') — otherwise 'export { default as HeartIcon }' is never found.
export { default as HeartIcon } from './heart.svg';
export { default as ShieldIcon } from './shield.svg';
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { transformSync } from '@babel/core';
import * as babelTypes from '@babel/types';
import glob from 'fast-glob';
import fs from 'fs';
import path from 'path';
Expand Down Expand Up @@ -296,7 +297,7 @@ function mergeSvgAssets(assetsDir: string) {
* --followSymlinks Follow symbolic links during directory traversal.
* Default: false (symlinks are ignored).
*/
function generateSessionReplayAssets() {
export function generateSessionReplayAssets() {
const cliOptions = parseCliArgs();
const { verbose } = cliOptions;

Expand Down Expand Up @@ -370,6 +371,8 @@ function generateSessionReplayAssets() {
const errors: Array<{ file: string; error: string }> = [];

const reactNativeSVG = new ReactNativeSVG(rootDir, assetsPath, true);
reactNativeSVG.setApiTypes(babelTypes);
reactNativeSVG.buildSvgMap();

for (const file of files) {
try {
Expand Down
12 changes: 10 additions & 2 deletions packages/react-native-babel-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,17 @@ export default declare(
assetsPath = getAssetsPath();
}

reactNativeSVG = options.__internal_reactNativeSVG;
// Reuse the instance across files instead of rebuilding (and
// rescanning) it per file.
reactNativeSVG =
options.__internal_reactNativeSVG ?? reactNativeSVG;
if (!reactNativeSVG && assetsPath) {
reactNativeSVG = new ReactNativeSVG(
process.cwd(),
assetsPath,
options.__internal_saveSvgMapToDisk || false
);
reactNativeSVG.setApiTypes(api.types);
reactNativeSVG.buildSvgMap();
Comment thread
jonathanmos marked this conversation as resolved.
}
reactNativeSVG?.setApiTypes(api.types);
Expand Down Expand Up @@ -141,7 +145,11 @@ export default declare(
options
);

pluginState.reactNativeSVG?.processItem(path, name);
pluginState.reactNativeSVG?.processItem(
path,
name,
state.filename ?? ''
);
}
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,40 @@ type Dependencies = {
t: typeof Babel.types;
path: Babel.NodePath<Babel.types.JSXElement>;
name: string;
localSvgMap: Record<string, { path: string; content?: string }>;
// Already scoped to the current file's actual import bindings (see
// ReactNativeSVG.resolveSvgImport) — null if this tag isn't a local SVG import.
svgPath: string | null;
readSvgContent: (path: string) => string;
};

export class HandlerResolver {
private static registry: Record<string, Resolver>;
private static dependencies: Dependencies | null = null;

/**
* Registers handler factories for supported JSX element types and stores shared dependencies.
* This method must be called before invoking `create()`, as it initializes the internal registry
* with handler constructors that are parameterized with the provided Babel context and configuration.
*
* @param dependencies - Shared Babel-related dependencies and contextual information,
* including `types`, the current JSX `path`, tag `name`, and the `localSvgMap`.
*/
static configure(dependencies: Dependencies) {
this.dependencies = dependencies;
const { t, path, name, localSvgMap } = dependencies;
const { t, path, name, svgPath, readSvgContent } = dependencies;

HandlerResolver.registry = {
RNSvgHandler: () => new RNSvgHandler(t, path, name),
// UriSvgHandler: () => new UriSvgHandler(t, path, name),
LocalSvgHandler: () =>
new LocalSvgHandler(t, path, name, localSvgMap)
new LocalSvgHandler(
t,
path,
name,
svgPath as string,
readSvgContent
)
};
}

/**
* Resolves and returns the appropriate handler instance based on the JSX tag name.
* @throws Error if `configure()` has not been called prior to invocation.
*
* @returns The resolved handler instance or `null` if no match exists.
*/
static create() {
if (!this.dependencies) {
throw new Error('HandlerResolver must be configured before use.');
}

const { name, localSvgMap } = this.dependencies;
const { name, svgPath } = this.dependencies;

switch (name) {
case 'Svg': {
Expand All @@ -66,7 +61,7 @@ export class HandlerResolver {
// }

default: {
return localSvgMap[name]
return svgPath
? HandlerResolver.registry.LocalSvgHandler()
: null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,36 @@
*/

import type * as Babel from '@babel/core';
import fs from 'fs';

import { getNodeName } from '../../../utils';
import { handleSvgDimensions } from '../processing/attributes';

import type { SvgHandler } from './SvgHandler';

/**
* Internal handler that inlines locally imported SVG components into
* JSX output during Babel transformation.
*
* The `LocalSvgHandler` resolves SVG imports from disk, caches their raw
* contents, and extracts relevant dimension attributes (e.g., width, height)
* from the JSX element for use in the generated SVG markup.
* Inlines locally imported SVG components into JSX output during Babel
* transformation.
*/
export class LocalSvgHandler implements SvgHandler {
constructor(
private types: typeof Babel.types,
private path: Babel.NodePath<Babel.types.JSXElement>,
private name: string,
private localSvgMap: Record<string, { path: string; content?: string }>
private svgPath: string,
private readSvgContent: (path: string) => string
) {
// no-op
}

/**
* Retrieves and returns the contents of a local SVG file corresponding to the JSXElement tag name.
* If the file hasn't been read yet, it reads the SVG content from disk and caches it in `localSvgMap`.
* Also extracts and stores width/height dimensions from the JSX attributes into the `dimensions` object.
*
* @param dimensions - Object to collect extracted width/height info.
* @returns Raw SVG string content from the local file, or undefined if the tag is not found in `localSvgMap`.
*/
transformSvgNode(dimensions: Record<string, string>) {
if (!this.localSvgMap[this.name]) {
if (!this.svgPath) {
return undefined;
}

const { path, content } = this.localSvgMap[this.name];
const content = this.readSvgContent(this.svgPath);

if (!content) {
this.localSvgMap[this.name].content = fs.readFileSync(path, 'utf8');
return undefined;
}

this.processAttributes(
Expand All @@ -56,22 +44,9 @@ export class LocalSvgHandler implements SvgHandler {
dimensions
);

return this.localSvgMap[this.name].content;
return content;
}

/**
* Processes the attributes of a JSXElement to extract relevant SVG metadata.
* Specifically identifies and handles dimension-related attributes (e.g., width, height),
* storing them into the provided `dimensions` object. Ignores spread attributes.
*
* @param t - Babel types helper.
*
* @param rootElementPath - The path of the root JSX element containing the SVG.
* Used to locate lexical scopes (component or program) for resolving variable references.
* May be `null` if no traversal context is available.
* @param jsxElement - The JSXElement whose attributes will be processed.
* @param dimensions - Object to collect extracted width/height info.
*/
private processAttributes(
t: typeof Babel.types,
rootElementPath: Babel.NodePath<Babel.types.JSXElement> | null,
Expand All @@ -90,7 +65,6 @@ export class LocalSvgHandler implements SvgHandler {
continue;
}

// Handle SVG dimensions
handleSvgDimensions(
t,
rootElementPath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class UriSvgHandler implements SvgHandler {
* Also extracts and stores width/height dimensions from the JSX attributes into the `dimensions` object.
*
* @param dimensions - Object to collect extracted width/height info.
* @returns Raw SVG string content from the local file, or undefined if the tag is not found in `localSvgMap`.
* @returns The `uri` attribute value, or undefined if the element has none.
*/
transformSvgNode(dimensions: Record<string, string>) {
const uri = this.processAttributes(
Expand Down
Loading