Skip to content
Merged
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
303 changes: 285 additions & 18 deletions packages/react-native-babel-plugin/src/cli/generate-sr-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
clearAssetsDir,
getAssetsPath
} from '../libraries/react-native-svg/processing/fs';
import { ReactNativeSVG } from '../libraries/react-native-svg';

type SvgIndexEntry = {
offset: number;
Expand All @@ -23,6 +24,182 @@ type SvgIndexEntry = {

type SvgIndex = Record<string, SvgIndexEntry>;

export type CliOptions = {
ignore: string[];
verbose: boolean;
path: string | null;
followSymlinks: boolean;
};

/**
* Converts a user-provided ignore pattern to a glob pattern.
*
* Handling rules:
* - Glob patterns (containing * ? [ ] { } ( )) are left unchanged
* - Absolute paths are appended with /** if not already present
* - Relative paths (./ or ../ or containing path separator) are resolved to absolute and appended with /**
* - Simple folder names (no slashes, no globs) are wrapped with ** /name/** for wildcard matching
*
* @param pattern - The user-provided pattern
* @param cwd - The base directory for resolving relative paths (required)
* @returns A glob-compatible pattern
*/
export function normalizeIgnorePattern(pattern: string, cwd: string): string {
// Detect glob patterns - leave unchanged
const isGlob = /[*?[\]{}()]/.test(pattern);
if (isGlob) {
return pattern;
}

const isAbsolute = path.isAbsolute(pattern);
const isRelative =
pattern.startsWith('./') ||
pattern.startsWith('../') ||
pattern.startsWith('.\\') ||
pattern.startsWith('..\\') ||
pattern.includes(path.sep) ||
pattern.includes('/'); // Also check for forward slash on all platforms

// Absolute paths - append /** if needed
if (isAbsolute) {
if (pattern.endsWith('/**') || pattern.endsWith(`${path.sep}**`)) {
return pattern;
}
return path.join(pattern, '**');
}

// Relative paths - resolve to absolute and append /**
if (isRelative) {
const resolved = path.resolve(cwd, pattern);

if (resolved.endsWith('/**') || resolved.endsWith(`${path.sep}**`)) {
return resolved;
}

return path.join(resolved, '**');
}

// Simple folder names (no slashes, no glob characters) - wildcard match
return `**/${pattern}/**`;
}

export const DEFAULT_IGNORE_PATTERNS = [
// Dependencies and build output
'**/node_modules/**',
'**/lib/**',
'**/dist/**',
'**/build/**',
'**/vendor/**',
// Native code (not React components)
'**/ios/**',
'**/android/**',
'**/Pods/**',
// Expo
'**/.expo/**',
// Cache and metadata
'**/.git/**',
'**/.cache/**',
'**/.yarn/**',
// TypeScript declarations
'**/*.d.ts',
// Test files and directories
'**/*.test.*',
'**/*.spec.*',
'**/__tests__/**',
'**/__mocks__/**',
'**/__snapshots__/**',
'**/coverage/**',
// Config files
'**/*.config.js',
'**/*.config.ts'
];

/**
* Parses command line arguments for the generate-sr-assets CLI.
*
* Supported arguments:
* --ignore <pattern> Additional glob patterns to ignore (can be specified multiple times)
* --verbose, -v Enable verbose output for debugging
* --path, -p <path> Path to the root directory to scan (defaults to current working directory)
* --followSymlinks Follow symbolic links during traversal (default: false)
* --help, -h Show help message
*
* @param args - Optional array of arguments (defaults to process.argv.slice(2))
* @returns Parsed CLI options
*/
export function parseCliArgs(args?: string[]): CliOptions {
const argv = args ?? process.argv.slice(2);
const options: CliOptions = {
ignore: [],
verbose: false,
path: null,
followSymlinks: false
};

for (let i = 0; i < argv.length; i++) {
const arg = argv[i];

if (arg === '--help' || arg === '-h') {
printHelp();
process.exit(0);
} else if (arg === '--ignore' || arg === '-i') {
const value = argv[++i];
if (value && !value.startsWith('-')) {
options.ignore.push(value);
} else {
console.warn(
'Warning: --ignore flag requires a pattern argument'
);
i--; // Reprocess this arg if it's another flag
}
} else if (arg === '--verbose' || arg === '-v') {
options.verbose = true;
} else if (arg === '--path' || arg === '-p') {
const value = argv[++i];
if (value && !value.startsWith('-')) {
options.path = value;
} else {
console.warn('Warning: --path flag requires a directory path');
i--; // Reprocess this arg if it's another flag
}
} else if (arg === '--followSymlinks') {
options.followSymlinks = true;
}
}

return options;
}

/**
* Prints the help message for the CLI.
*/
function printHelp(): void {
console.info(`
Usage: npx datadog-generate-sr-assets [options]

Pre-generate SVG assets for Datadog Session Replay.

Options:
--ignore, -i <pattern> Additional patterns to ignore during scanning.
Can be a folder name or a glob pattern.
Folder names are auto-converted to glob patterns.
Can be specified multiple times.
--path, -p <path> Path to the root directory to scan.
Defaults to the current working directory.
--verbose, -v Enable verbose output for debugging.
--followSymlinks Follow symbolic links during directory traversal.
Default: false (symlinks are ignored).
--help, -h Show this help message.

Examples:
npx datadog-generate-sr-assets
npx datadog-generate-sr-assets --path ./src
npx datadog-generate-sr-assets --ignore legacy --ignore vendor
npx datadog-generate-sr-assets --ignore "**/custom-pattern/**" --verbose
npx datadog-generate-sr-assets -p ./src -i old-code -v
`);
}

/**
* Merges all individual SVG files into assets.bin and creates an index in assets.json.
* This function reads all .svg files from the assets directory and packs them into
Expand Down Expand Up @@ -104,43 +281,96 @@ function mergeSvgAssets(assetsDir: string) {
* references are available during the build process.
*
* Usage:
* npx @datadog/mobile-react-native-babel-plugin generate-sr-assets
* npx @datadog/mobile-react-native-babel-plugin generate-sr-assets [options]
* or
* npx datadog-generate-sr-assets
* npx datadog-generate-sr-assets [options]
*
* Options:
* --ignore, -i pattern Additional glob patterns to ignore during scanning.
* Can be specified multiple times.
* Example: --ignore "**\/legacy\/**" --ignore "**\/vendor\/**"
* --verbose, -v Enable verbose output for debugging.
* --path, -p path Path to the root directory to scan.
* Defaults to the current working directory.
* Example: --path ./src
* --followSymlinks Follow symbolic links during directory traversal.
* Default: false (symlinks are ignored).
*/
function generateSessionReplayAssets() {
const rootDir = process.cwd();
const cliOptions = parseCliArgs();
const { verbose } = cliOptions;

// Resolve the root directory from --path flag or default to cwd
const rootDir = cliOptions.path
? path.resolve(process.cwd(), cliOptions.path)
: process.cwd();

// Validate the path exists
if (cliOptions.path && !fs.existsSync(rootDir)) {
console.error(`Error: Path does not exist: ${rootDir}`);
process.exit(1);
}

if (cliOptions.path && !fs.statSync(rootDir).isDirectory()) {
console.error(`Error: Path is not a directory: ${rootDir}`);
process.exit(1);
}

const assetsPath = getAssetsPath();

const startTime = Date.now();

if (!assetsPath) {
if (verbose) {
console.info(
'[verbose] No assets path found. Session Replay module may not be installed.'
);
}
process.exit(0);
}

console.info(`Scanning for session replay assets in ${rootDir}...`);

if (verbose) {
console.info(`[verbose] Assets output path: ${assetsPath}`);
}

// Clear existing assets to ensure a fresh state
clearAssetsDir(assetsPath);

// Merge default ignore patterns with user-provided ones
// Convert folder names/paths to glob patterns based on type:
// - Simple names: "legacy" → "**/legacy/**"
// - Relative paths: "./android" → "/abs/path/android/**"
// - Absolute paths: "/home/dev/android" → "/home/dev/android/**"
const userIgnorePatterns = cliOptions.ignore.map(pattern =>
normalizeIgnorePattern(pattern, rootDir)
);
const ignorePatterns = [...DEFAULT_IGNORE_PATTERNS, ...userIgnorePatterns];

if (verbose) {
console.info(`[verbose] Follow symlinks: ${cliOptions.followSymlinks}`);
console.info('[verbose] Ignore patterns:');
ignorePatterns.forEach(pattern => console.info(` - ${pattern}`));
}

const files = glob.sync(['**/*.{js,jsx,ts,tsx}'], {
cwd: rootDir,
absolute: true,
ignore: [
'**/node_modules/**',
'**/lib/**',
'**/dist/**',
'**/build/**',
'**/*.d.ts',
'**/*.test.*',
'**/*.spec.*',
'**/*.config.js',
'**/__tests__/**',
'**/__mocks__/**'
]
ignore: ignorePatterns,
followSymbolicLinks: cliOptions.followSymlinks
});

if (verbose) {
console.info(`[verbose] Found ${files.length} files to scan`);
}

let errorCount = 0;
let processedCount = 0;
const errors: Array<{ file: string; error: string }> = [];

const reactNativeSVG = new ReactNativeSVG(rootDir, assetsPath, true);

for (const file of files) {
try {
const code = fs.readFileSync(file, 'utf8');
Expand All @@ -155,7 +385,8 @@ function generateSessionReplayAssets() {
sessionReplay: {
svgTracking: true
},
__internal_saveSvgMapToDisk: true
__internal_saveSvgMapToDisk: true,
__internal_reactNativeSVG: reactNativeSVG
}
]
],
Expand All @@ -170,11 +401,19 @@ function generateSessionReplayAssets() {
code: false,
ast: false
});

processedCount++;
} catch (error) {
errorCount++;
const errorMessage =
error instanceof Error ? error.message : String(error);
errors.push({ file, error: errorMessage });

if (verbose) {
const relativePath = path.relative(rootDir, file);
console.warn(`[verbose] Error processing ${relativePath}:`);
console.warn(` ${errorMessage}`);
}
}
}

Expand All @@ -185,6 +424,32 @@ function generateSessionReplayAssets() {
// Merge all individual SVG files into assets.bin and assets.json
mergeSvgAssets(assetsPath);

const duration = Date.now() - startTime;

if (verbose) {
console.info('\n[verbose] Summary:');
console.info(` Files scanned: ${files.length}`);
console.info(` Files processed successfully: ${processedCount}`);
console.info(` Files with errors: ${errorCount}`);
console.info(` Duration: ${duration}ms`);

if (errors.length > 0 && errors.length <= 10) {
console.info('\n[verbose] Files with errors:');
errors.forEach(({ file }) => {
const relativePath = path.relative(rootDir, file);
console.info(` - ${relativePath}`);
});
} else if (errors.length > 10) {
console.info(
`\n[verbose] ${errors.length} files had errors (showing first 10):`
);
errors.slice(0, 10).forEach(({ file }) => {
const relativePath = path.relative(rootDir, file);
console.info(` - ${relativePath}`);
});
}
}

if (errorCount > 0) {
console.info(
'Asset generation finished, but some files encountered errors.'
Expand All @@ -194,5 +459,7 @@ function generateSessionReplayAssets() {
console.info('Your assets are now ready to be used by Session Replay.');
}

// TODO: Add flag support [e.g., --verbose] (RUM-12186)
generateSessionReplayAssets();
// Only run when executed directly (not when imported for testing)
if (require.main === module) {
generateSessionReplayAssets();
}
Loading
Loading