UNPKG

@sentry/react-native

Version:
103 lines 5.66 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = sentryExpoRouterAutoWrapBabelPlugin; /** * Babel plugin that auto-wraps Expo Router's per-route `ErrorBoundary` so the * Sentry SDK captures render-phase errors that hit the fallback without * requiring the user to change their route file. * * It rewrites: * * ```ts * export { ErrorBoundary } from 'expo-router'; * ``` * * into: * * ```ts * import { ErrorBoundary as __sentryOriginalExpoErrorBoundary } from 'expo-router'; * import { wrapExpoRouterErrorBoundary as __sentryWrapExpoRouterErrorBoundary } from '@sentry/react-native'; * export const ErrorBoundary = __sentryWrapExpoRouterErrorBoundary(__sentryOriginalExpoErrorBoundary); * ``` * * Aliased re-exports (`export { ErrorBoundary as Foo } from 'expo-router'`) * are preserved — the wrapped export keeps the user-chosen name. Mixed * re-exports such as * `export { ErrorBoundary, Stack } from 'expo-router'` keep the non-boundary * specifiers in place. * * The transform is structurally idempotent: after the rewrite the re-export * is no longer an `export ... from 'expo-router'`, so a second pass over the * same file finds nothing to transform. * * Files inside `node_modules` are never transformed. */ const ORIGINAL_BOUNDARY_LOCAL = '__sentryOriginalExpoErrorBoundary'; const WRAP_FN_LOCAL = '__sentryWrapExpoRouterErrorBoundary'; const SENTRY_PACKAGE = '@sentry/react-native'; const EXPO_ROUTER_PACKAGE = 'expo-router'; const BOUNDARY_EXPORT = 'ErrorBoundary'; function sentryExpoRouterAutoWrapBabelPlugin({ types: t }) { return { name: 'sentry-expo-router-auto-wrap-error-boundary', visitor: { ExportNamedDeclaration(path, state) { var _a, _b, _c; const node = path.node; if (!node.source || node.source.value !== EXPO_ROUTER_PACKAGE) { return; } const filename = (_c = (_b = (_a = state.file) === null || _a === void 0 ? void 0 : _a.opts) === null || _b === void 0 ? void 0 : _b.filename) !== null && _c !== void 0 ? _c : ''; if (filename.includes('node_modules')) { return; } const boundarySpecifierIndex = node.specifiers.findIndex(s => t.isExportSpecifier(s) && t.isIdentifier(s.local) && s.local.name === BOUNDARY_EXPORT); if (boundarySpecifierIndex === -1) { return; } const boundarySpecifier = node.specifiers[boundarySpecifierIndex]; const exportedName = t.isIdentifier(boundarySpecifier.exported) ? boundarySpecifier.exported.name : boundarySpecifier.exported.value; // Hoist the two helper imports to the top of the Program body so // they sit alongside the file's other `import` declarations rather // than landing mid-file. Some toolchains (e.g. Hermes) are strict // about import placement, and mid-file imports are also harder to // read. Inject once per file so a second wrap reuses the bindings. const HELPERS_KEY = 'sentryAutoWrapHelpersInjected'; if (state.get(HELPERS_KEY) !== true) { const program = path.scope.getProgramParent().path; program.unshiftContainer('body', [ t.importDeclaration([t.importSpecifier(t.identifier(ORIGINAL_BOUNDARY_LOCAL), t.identifier(BOUNDARY_EXPORT))], t.stringLiteral(EXPO_ROUTER_PACKAGE)), t.importDeclaration([t.importSpecifier(t.identifier(WRAP_FN_LOCAL), t.identifier('wrapExpoRouterErrorBoundary'))], t.stringLiteral(SENTRY_PACKAGE)), ]); state.set(HELPERS_KEY, true); } // Generate a unique local binding for the wrapped boundary instead of // declaring `const <exportedName> = ...` directly. That avoids clashing // with an existing top-level binding of the same name in the file // (e.g. `import { ErrorBoundary } from 'expo-router'` used elsewhere) // which would otherwise produce a duplicate-binding compile error. const wrappedLocal = path.scope.generateUidIdentifier(`wrapped${exportedName}`); const replacements = [ t.variableDeclaration('const', [ t.variableDeclarator(wrappedLocal, t.callExpression(t.identifier(WRAP_FN_LOCAL), [t.identifier(ORIGINAL_BOUNDARY_LOCAL)])), ]), t.exportNamedDeclaration(null, [t.exportSpecifier(t.cloneNode(wrappedLocal), t.identifier(exportedName))]), ]; const remainingSpecifiers = node.specifiers .filter((_, i) => i !== boundarySpecifierIndex) // Clone the reused specifier nodes so we don't share AST nodes // between the original `path.node` (about to be replaced) and the // new export declaration — Babel docs warn that reusing nodes can // corrupt the scope cache. .map(s => t.cloneNode(s)); if (remainingSpecifiers.length > 0) { replacements.push(t.exportNamedDeclaration(null, remainingSpecifiers, t.cloneNode(node.source))); } path.replaceWithMultiple(replacements); }, }, }; } //# sourceMappingURL=sentryExpoRouterAutoWrapBabelPlugin.js.map