|  | 
|  | 1 | +import { logger } from '@sentry/core'; | 
|  | 2 | +import * as fs from 'fs'; | 
|  | 3 | +import type { MetroConfig, Module } from 'metro'; | 
|  | 4 | +// eslint-disable-next-line import/no-extraneous-dependencies | 
|  | 5 | +import * as countLines from 'metro/src/lib/countLines'; | 
|  | 6 | +import * as path from 'path'; | 
|  | 7 | + | 
|  | 8 | +import type { MetroCustomSerializer, VirtualJSOutput } from './utils'; | 
|  | 9 | +import { createSet } from './utils'; | 
|  | 10 | + | 
|  | 11 | +const DEFAULT_OPTIONS_FILE_NAME = 'sentry.options.json'; | 
|  | 12 | + | 
|  | 13 | +/** | 
|  | 14 | + * Loads Sentry options from a file in | 
|  | 15 | + */ | 
|  | 16 | +export function withSentryOptionsFromFile(config: MetroConfig, optionsFile: string | boolean): MetroConfig { | 
|  | 17 | +  if (optionsFile === false) { | 
|  | 18 | +    return config; | 
|  | 19 | +  } | 
|  | 20 | + | 
|  | 21 | +  const { projectRoot } = config; | 
|  | 22 | +  if (!projectRoot) { | 
|  | 23 | +    // eslint-disable-next-line no-console | 
|  | 24 | +    console.error('[@sentry/react-native/metro] Project root is required to load Sentry options from a file'); | 
|  | 25 | +    return config; | 
|  | 26 | +  } | 
|  | 27 | + | 
|  | 28 | +  let optionsPath = path.join(projectRoot, DEFAULT_OPTIONS_FILE_NAME); | 
|  | 29 | +  if (typeof optionsFile === 'string' && path.isAbsolute(optionsFile)) { | 
|  | 30 | +    optionsPath = optionsFile; | 
|  | 31 | +  } else if (typeof optionsFile === 'string') { | 
|  | 32 | +    optionsPath = path.join(projectRoot, optionsFile); | 
|  | 33 | +  } | 
|  | 34 | + | 
|  | 35 | +  const originalSerializer = config.serializer?.customSerializer; | 
|  | 36 | +  if (!originalSerializer) { | 
|  | 37 | +    // It's okay to bail here because we don't expose this for direct usage, but as part of `withSentryConfig` | 
|  | 38 | +    // If used directly in RN, the user is responsible for providing a custom serializer first, Expo provides serializer in default config | 
|  | 39 | +    // eslint-disable-next-line no-console | 
|  | 40 | +    console.error( | 
|  | 41 | +      '[@sentry/react-native/metro] `config.serializer.customSerializer` is required to load Sentry options from a file', | 
|  | 42 | +    ); | 
|  | 43 | +    return config; | 
|  | 44 | +  } | 
|  | 45 | + | 
|  | 46 | +  const sentryOptionsSerializer: MetroCustomSerializer = (entryPoint, preModules, graph, options) => { | 
|  | 47 | +    const sentryOptionsModule = createSentryOptionsModule(optionsPath); | 
|  | 48 | +    if (sentryOptionsModule) { | 
|  | 49 | +      (preModules as Module[]).push(sentryOptionsModule); | 
|  | 50 | +    } | 
|  | 51 | +    return originalSerializer(entryPoint, preModules, graph, options); | 
|  | 52 | +  }; | 
|  | 53 | + | 
|  | 54 | +  return { | 
|  | 55 | +    ...config, | 
|  | 56 | +    serializer: { | 
|  | 57 | +      ...config.serializer, | 
|  | 58 | +      customSerializer: sentryOptionsSerializer, | 
|  | 59 | +    }, | 
|  | 60 | +  }; | 
|  | 61 | +} | 
|  | 62 | + | 
|  | 63 | +function createSentryOptionsModule(filePath: string): Module<VirtualJSOutput> | null { | 
|  | 64 | +  let content: string; | 
|  | 65 | +  try { | 
|  | 66 | +    content = fs.readFileSync(filePath, 'utf8'); | 
|  | 67 | +  } catch (error) { | 
|  | 68 | +    if ((error as NodeJS.ErrnoException).code === 'ENOENT') { | 
|  | 69 | +      logger.debug(`[@sentry/react-native/metro] Sentry options file does not exist at ${filePath}`); | 
|  | 70 | +    } else { | 
|  | 71 | +      logger.error(`[@sentry/react-native/metro] Failed to read Sentry options file at ${filePath}`); | 
|  | 72 | +    } | 
|  | 73 | +    return null; | 
|  | 74 | +  } | 
|  | 75 | + | 
|  | 76 | +  let parsedContent: Record<string, unknown>; | 
|  | 77 | +  try { | 
|  | 78 | +    parsedContent = JSON.parse(content); | 
|  | 79 | +  } catch (error) { | 
|  | 80 | +    logger.error(`[@sentry/react-native/metro] Failed to parse Sentry options file at ${filePath}`); | 
|  | 81 | +    return null; | 
|  | 82 | +  } | 
|  | 83 | + | 
|  | 84 | +  const minifiedContent = JSON.stringify(parsedContent); | 
|  | 85 | +  const optionsCode = `var __SENTRY_OPTIONS__=${minifiedContent};`; | 
|  | 86 | + | 
|  | 87 | +  logger.debug(`[@sentry/react-native/metro] Sentry options added to the bundle from file at ${filePath}`); | 
|  | 88 | +  return { | 
|  | 89 | +    dependencies: new Map(), | 
|  | 90 | +    getSource: () => Buffer.from(optionsCode), | 
|  | 91 | +    inverseDependencies: createSet(), | 
|  | 92 | +    path: '__sentry-options__', | 
|  | 93 | +    output: [ | 
|  | 94 | +      { | 
|  | 95 | +        type: 'js/script/virtual', | 
|  | 96 | +        data: { | 
|  | 97 | +          code: optionsCode, | 
|  | 98 | +          lineCount: countLines(optionsCode), | 
|  | 99 | +          map: [], | 
|  | 100 | +        }, | 
|  | 101 | +      }, | 
|  | 102 | +    ], | 
|  | 103 | +  }; | 
|  | 104 | +} | 
0 commit comments