UNPKG

@callstack/repack-dev-server

Version:

A bundler-agnostic development server for React Native applications as part of @callstack/repack.

191 lines (190 loc) 7.2 kB
import { URL } from 'node:url'; import { codeFrameColumns } from '@babel/code-frame'; import { SourceMapConsumer } from 'source-map'; /** * Class for transforming stack traces from React Native application with using Source Map. * Raw stack frames produced by React Native, points to some location from the bundle * eg `index.bundle?platform=ios:567:1234`. By using Source Map for that bundle `Symbolicator` * produces frames that point to source code inside your project eg `Hello.tsx:10:9`. */ export class Symbolicator { /** * Infer platform from stack frames. * Usually at least one frame has `file` field with the bundle URL eg: * `http://localhost:8081/index.bundle?platform=ios&...`, which can be used to infer platform. * * @param stack Array of stack frames. * @returns Inferred platform or `undefined` if cannot infer. */ static inferPlatformFromStack(stack) { for (const frame of stack) { if (!frame.file) { continue; } const { searchParams, pathname } = new URL(frame.file, 'file://'); const platform = searchParams.get('platform'); if (platform) { return platform; } const [bundleFilename] = pathname.split('/').reverse(); const [, platformOrExtension, extension] = bundleFilename.split('.'); if (extension) { return platformOrExtension; } } } /** * Constructs new `Symbolicator` instance. * * @param delegate Delegate instance with symbolication functions. */ constructor(delegate) { this.delegate = delegate; /** * Cache with initialized `SourceMapConsumer` to improve symbolication performance. */ this.sourceMapConsumerCache = {}; } /** * Process raw React Native stack frames and transform them using Source Maps. * Method will try to symbolicate as much data as possible, but if the Source Maps * are not available, invalid or the original positions/data is not found in Source Maps, * the method will return raw values - the same as supplied with `stack` parameter. * For example out of 10 frames, it's possible that only first 7 will be symbolicated and the * remaining 3 will be unchanged. * * @param logger Fastify logger instance. * @param stack Raw stack frames. * @returns Symbolicated stack frames. */ async process(logger, stack) { logger.debug({ msg: 'Filtering out unnecessary frames' }); const frames = []; for (const frame of stack) { const { file } = frame; if (file?.startsWith('http')) { frames.push(frame); } } try { logger.debug({ msg: 'Processing frames', frames }); const processedFrames = []; for (const frame of frames) { if (!this.sourceMapConsumerCache[frame.file]) { logger.debug({ msg: 'Loading raw source map data', fileUrl: frame.file, }); const rawSourceMap = await this.delegate.getSourceMap(frame.file); logger.debug({ msg: 'Creating source map instance', fileUrl: frame.file, sourceMapLength: rawSourceMap.length, }); const sourceMapConsumer = await new SourceMapConsumer(rawSourceMap.toString()); logger.debug({ msg: 'Saving source map instance into cache', fileUrl: frame.file, }); this.sourceMapConsumerCache[frame.file] = sourceMapConsumer; } logger.debug({ msg: 'Symbolicating frame', frame, }); const processedFrame = this.processFrame(frame); logger.debug({ msg: 'Finished symbolicating frame', frame, }); processedFrames.push(processedFrame); } const codeFrame = (await this.getCodeFrame(logger, processedFrames)) ?? null; logger.debug({ msg: 'Finished symbolicating frames', processedFrames, codeFrame, }); return { stack: processedFrames, codeFrame, }; } finally { for (const key in this.sourceMapConsumerCache) { this.sourceMapConsumerCache[key].destroy(); delete this.sourceMapConsumerCache[key]; } } } processFrame(frame) { if (!frame.lineNumber || !frame.column) { return { ...frame, collapse: false, }; } const consumer = this.sourceMapConsumerCache[frame.file]; if (!consumer) { return { ...frame, collapse: false, }; } const lookup = consumer.originalPositionFor({ line: frame.lineNumber, column: frame.column, bias: SourceMapConsumer.LEAST_UPPER_BOUND, }); // If lookup fails, we get the same shape object, but with // all values set to null if (!lookup.source) { // It is better to gracefully return the original frame // than to throw an exception return { ...frame, collapse: false, }; } return { lineNumber: lookup.line || frame.lineNumber, column: lookup.column || frame.column, file: lookup.source, methodName: lookup.name || frame.methodName, collapse: false, }; } async getCodeFrame(logger, processedFrames) { for (const frame of processedFrames) { if (frame.collapse || !frame.lineNumber || !frame.column) { continue; } if (!this.delegate.shouldIncludeFrame(frame)) { return undefined; } logger.debug({ msg: 'Generating code frame', frame, }); try { return { content: codeFrameColumns((await this.delegate.getSource(frame.file)).toString(), { start: { column: frame.column, line: frame.lineNumber }, }, { forceColor: true }), location: { row: frame.lineNumber, column: frame.column, }, fileName: frame.file, }; } catch (error) { logger.error({ msg: 'Failed to create code frame', error: error.message, }); } return undefined; } } }