diff --git a/react_juce/core/EcmascriptEngine.cpp b/react_juce/core/EcmascriptEngine.cpp index ed24c324..f24059fb 100644 --- a/react_juce/core/EcmascriptEngine.cpp +++ b/react_juce/core/EcmascriptEngine.cpp @@ -96,4 +96,140 @@ namespace reactjuce mPimpl->debuggerDetach(); } + //============================================================================== + SourceMap::SourceMap(const juce::String& source, const juce::String& map) + : mapLoaded(false) + , sourcePath(source) + { + mapLoaded = LoadMap(map); + } + + SourceMap::SourceMap(const juce::String& source, const juce::File& map) + : mapLoaded(false) + , sourcePath(source) + { + if (map.existsAsFile()) + mapLoaded = LoadMap(map.loadFileAsString()); + } + + SourceMap::Location SourceMap::translate(int line, int col) const + { + // line and col are 1-based. + SourceMap::Location failRes{sourcePath, line, col}; + if (!mapLoaded || line <= 0 || col <= 0 || static_cast(line) > mappings.size()) + return failRes; + + const Segment* use_segment = nullptr; + for (const auto& segment : mappings[line - 1]) + if (segment.segStartCol >= col) + break; + else + use_segment = &segment; + + if (!use_segment || !use_segment->gotSourceMap || use_segment->origSourceId < 0 || + static_cast(use_segment->origSourceId) >= sources.size()) + return failRes; + + return + { + sources[use_segment->origSourceId], + use_segment->origStartLine + 1, + col - use_segment->segStartCol + use_segment->origStartCol + }; + } + + bool SourceMap::LoadMap(const juce::String& map) + { + auto json = juce::JSON::parse(map); + const auto fileVersion = json.getProperty("version", juce::var()); + const auto fileMappings = json.getProperty("mappings", juce::var()); + auto fileSources = json.getProperty("sources", juce::var()); + + if (!fileVersion.isInt() || static_cast(fileVersion) != 3 || + !fileMappings.isString() || !fileSources.isArray() ) + return false; + + for (auto &i : *fileSources.getArray()) + { + if (!i.isString()) + return false; + sources.emplace_back(std::move(i.toString())); + } + + bool firstSegmentInLine = true; + std::vector lineSegments; + Segment segment; + Segment lastSegment; + int segmentIntNo = 0; + int segmentIntVal = 0; + int segmentIntCharNo = 0; + + for (auto p = fileMappings.toString().toRawUTF8(); *p; ++p) + { + if (*p == ',' || *p == ';') + { + segmentIntNo = segmentIntVal = segmentIntCharNo = 0; + if (firstSegmentInLine) + firstSegmentInLine = false; + else + segment.segStartCol += lastSegment.segStartCol; + segment.origSourceId += lastSegment.origSourceId; + segment.origStartLine += lastSegment.origStartLine; + segment.origStartCol += lastSegment.origStartCol; + lineSegments.push_back(segment); + lastSegment = segment; + segment = Segment(); + + if (*p == ';') + { + mappings.emplace_back(std::move(lineSegments)); + firstSegmentInLine = true; + } + } + else + { + static const std::string base64_charset = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + auto i = base64_charset.find(*p); + if (i == std::string::npos) + return false; + segmentIntVal |= ((i & 0x1f) << (segmentIntCharNo * 5)); + if (i & 0x20) + ++segmentIntCharNo; + else + { + bool neg = (segmentIntVal & 1); + segmentIntVal >>= 1; + if (neg) + segmentIntVal = -segmentIntVal; + switch(segmentIntNo++) + { + case 0: + segment.segStartCol = segmentIntVal; + break; + + case 1: + segment.origSourceId = segmentIntVal; + segment.gotSourceMap = true; + break; + + case 2: + segment.origStartLine = segmentIntVal; + break; + + case 3: + segment.origStartCol = segmentIntVal; + break; + } + segmentIntVal = segmentIntCharNo = 0; + } + } + } + // In case there's no final terminator. + mappings.emplace_back(std::move(lineSegments)); + + return true; + } } diff --git a/react_juce/core/EcmascriptEngine.h b/react_juce/core/EcmascriptEngine.h index ac8cd062..a9c00b8f 100644 --- a/react_juce/core/EcmascriptEngine.h +++ b/react_juce/core/EcmascriptEngine.h @@ -10,7 +10,7 @@ #pragma once #include - +#include namespace reactjuce { @@ -178,4 +178,41 @@ namespace reactjuce return invoke(name, vargs); } + //============================================================================== + /** Utility helper class for Ecmascript implementations. Read a SourceMap and + * provide translations to original source location. + */ + class SourceMap + { + public: + struct Location + { + const juce::String& file; + int line; + int col; + }; + + SourceMap(const juce::String& source, const juce::String& map); + SourceMap(const juce::String& source, const juce::File& map); + ~SourceMap() = default; + + Location translate(int line, int col) const; + + private: + struct Segment + { + int segStartCol = 0; + int origSourceId = 0; + int origStartLine = 0; + int origStartCol = 0; + bool gotSourceMap = false; + }; + + bool LoadMap(const juce::String& map); + + bool mapLoaded; + juce::String sourcePath; + std::vector sources; + std::vector> mappings; + }; } diff --git a/react_juce/core/EcmascriptEngine_Hermes.cpp b/react_juce/core/EcmascriptEngine_Hermes.cpp index 4a309651..5c0959a4 100644 --- a/react_juce/core/EcmascriptEngine_Hermes.cpp +++ b/react_juce/core/EcmascriptEngine_Hermes.cpp @@ -1,3 +1,5 @@ +#include + #include "EcmascriptEngine.h" #if _MSC_VER @@ -247,6 +249,34 @@ namespace reactjuce return {}; } + //============================================================================== + juce::String sourceMapError(const juce::String &err) + { + std::unordered_map> maps; + std::string s = err.toStdString(); + juce::String res; + const std::regex fileRef("\\((.*):([0-9]+):([0-9]+)\\)", std::regex_constants::ECMAScript); + std::smatch m; + + while (std::regex_search(s, m, fileRef)) + { + const juce::String source = m[1].str(); + const int line = std::stoi(m[2].str()); + const int col = std::stoi(m[3].str()); + + if (maps.find(source) == maps.end()) + maps.insert({source, std::make_unique(source, juce::File(source + ".map"))}); + const auto newloc = maps[source]->translate(line, col); + juce::String file = newloc.file; + if (file.startsWith("webpack:///")) + file = newloc.file.replaceFirstOccurrenceOf("webpack:///", ""); + + res += juce::String(m.prefix().str()) + "(" + file + ":" + juce::String(newloc.line) + ":" + juce::String(newloc.col) + ")"; + s = m.suffix(); + } + return res + s; + } + //============================================================================== } @@ -350,7 +380,7 @@ namespace reactjuce } catch (const jsi::JSIException &e) { - throw Error(e.what()); + throw Error(sourceMapError(e.what())); } } @@ -366,7 +396,7 @@ namespace reactjuce } catch (const jsi::JSIException &e) { - throw Error(e.what()); + throw Error(sourceMapError(e.what())); } } @@ -385,7 +415,7 @@ namespace reactjuce } catch (const jsi::JSIException &e) { - throw Error(e.what()); + throw Error(sourceMapError(e.what())); } } @@ -451,7 +481,7 @@ namespace reactjuce } catch(const jsi::JSIException &e) { - throw Error(e.what()); + throw Error(sourceMapError(e.what())); } }