|
1 | 1 | #include <Foundation/Foundation.h> |
2 | 2 | #include <notify.h> |
| 3 | +#include <algorithm> |
3 | 4 | #include <chrono> |
4 | 5 |
|
5 | 6 | #include "src/inspector/v8-console-message.h" |
|
31 | 32 | // dodges the libc++ deprecation that prompted the inspector's |
32 | 33 | // `UChar = uint16_t -> char16_t` switch. |
33 | 34 | StringView Make8BitStringView(const std::string& value) { |
34 | | - return StringView(reinterpret_cast<const uint8_t*>(value.data()), |
35 | | - value.size()); |
| 35 | + return StringView(reinterpret_cast<const uint8_t*>(value.data()), value.size()); |
| 36 | +} |
| 37 | + |
| 38 | +// Scheme advertised to the frontend for source maps the runtime can serve. |
| 39 | +// Chrome DevTools never loads `file:` (or `data:`/`devtools:`) resources |
| 40 | +// through the target -- PageResourceLoader routes those to the frontend host |
| 41 | +// machine, which cannot see files on the device. Any other scheme is fetched |
| 42 | +// with Network.loadNetworkResource, which we answer from disk. |
| 43 | +constexpr const char* kSourceMapScheme = "nsruntime://"; |
| 44 | + |
| 45 | +// Opt-out via nativescript.config.ts (serialized into the bundled |
| 46 | +// package.json): `ios: { disableSourceMapURLRewrite: true }`, or the same key |
| 47 | +// at the top level. |
| 48 | +bool ShouldRewriteSourceMapURLs() { |
| 49 | + static bool disabled = []() { |
| 50 | + id ios = tns::Runtime::GetAppConfigValue("ios"); |
| 51 | + id value = [ios isKindOfClass:[NSDictionary class]] ? ios[@"disableSourceMapURLRewrite"] : nil; |
| 52 | + if (value == nil) { |
| 53 | + value = tns::Runtime::GetAppConfigValue("disableSourceMapURLRewrite"); |
| 54 | + } |
| 55 | + return value != nil && [value boolValue]; |
| 56 | + }(); |
| 57 | + return !disabled; |
| 58 | +} |
| 59 | + |
| 60 | +// Rewrites the sourceMapURL of outgoing Debugger.scriptParsed / |
| 61 | +// Debugger.scriptFailedToParse events from a file url (or a url relative to |
| 62 | +// the script's file url) to an absolute nsruntime:// url, so DevTools |
| 63 | +// requests the map through the target instead of the frontend host. |
| 64 | +std::string MaybeRewriteSourceMapURL(const std::string& message) { |
| 65 | + if (!ShouldRewriteSourceMapURLs()) { |
| 66 | + return message; |
| 67 | + } |
| 68 | + |
| 69 | + if (message.find("\"Debugger.scriptParsed\"") == std::string::npos && |
| 70 | + message.find("\"Debugger.scriptFailedToParse\"") == std::string::npos) { |
| 71 | + return message; |
| 72 | + } |
| 73 | + |
| 74 | + auto parsed = json::parse(message, nullptr, false); |
| 75 | + if (parsed.is_discarded() || !parsed.contains("params")) { |
| 76 | + return message; |
| 77 | + } |
| 78 | + |
| 79 | + auto& params = parsed["params"]; |
| 80 | + std::string sourceMapURL = params.value("sourceMapURL", ""); |
| 81 | + if (sourceMapURL.empty() || sourceMapURL.rfind("data:", 0) == 0 || |
| 82 | + sourceMapURL.rfind("http:", 0) == 0 || sourceMapURL.rfind("https:", 0) == 0 || |
| 83 | + sourceMapURL.rfind(kSourceMapScheme, 0) == 0) { |
| 84 | + return message; |
| 85 | + } |
| 86 | + |
| 87 | + std::string path; |
| 88 | + if (sourceMapURL.rfind("file://", 0) == 0) { |
| 89 | + path = sourceMapURL.substr(strlen("file://")); |
| 90 | + } else if (sourceMapURL[0] == '/') { |
| 91 | + path = sourceMapURL; |
| 92 | + } else { |
| 93 | + // Relative to the script url, e.g. "bundle.js.map". |
| 94 | + std::string scriptUrl = params.value("url", ""); |
| 95 | + if (scriptUrl.rfind("file://", 0) != 0) { |
| 96 | + return message; |
| 97 | + } |
| 98 | + @autoreleasepool { |
| 99 | + NSString* scriptPath = |
| 100 | + [NSString stringWithUTF8String:scriptUrl.substr(strlen("file://")).c_str()]; |
| 101 | + NSString* mapPath = [NSString stringWithUTF8String:sourceMapURL.c_str()]; |
| 102 | + if (scriptPath != nil && mapPath != nil) { |
| 103 | + NSString* resolved = [[[scriptPath stringByDeletingLastPathComponent] |
| 104 | + stringByAppendingPathComponent:mapPath] stringByStandardizingPath]; |
| 105 | + if (resolved != nil) { |
| 106 | + path = [resolved UTF8String]; |
| 107 | + } |
| 108 | + } |
| 109 | + } |
| 110 | + } |
| 111 | + |
| 112 | + if (path.empty()) { |
| 113 | + return message; |
| 114 | + } |
| 115 | + |
| 116 | + params["sourceMapURL"] = kSourceMapScheme + path; |
| 117 | + return parsed.dump(); |
36 | 118 | } |
37 | 119 | } // namespace |
38 | 120 |
|
@@ -278,7 +360,7 @@ StringView Make8BitStringView(const std::string& value) { |
278 | 360 |
|
279 | 361 | void JsV8InspectorClient::notify(const std::string& message) { |
280 | 362 | if (this->sender_) { |
281 | | - this->sender_(message); |
| 363 | + this->sender_(MaybeRewriteSourceMapURL(message)); |
282 | 364 | } |
283 | 365 | } |
284 | 366 |
|
@@ -340,6 +422,40 @@ StringView Make8BitStringView(const std::string& value) { |
340 | 422 | return; |
341 | 423 | } |
342 | 424 |
|
| 425 | + // Chrome DevTools fetches source maps through the target: it sends |
| 426 | + // Network.loadNetworkResource for the resolved sourceMappingURL and reads |
| 427 | + // the returned stream with IO.read/IO.close. Neither domain is implemented |
| 428 | + // by V8's inspector, so handle them here. |
| 429 | + if (method == "Network.loadNetworkResource") { |
| 430 | + std::string url; |
| 431 | + if (json_message.contains("params") && json_message["params"].contains("url")) { |
| 432 | + url = json_message["params"]["url"].get<std::string>(); |
| 433 | + } |
| 434 | + this->HandleLoadNetworkResource(json_message["id"].get<int>(), url); |
| 435 | + return; |
| 436 | + } |
| 437 | + |
| 438 | + if (method == "IO.read" || method == "IO.close") { |
| 439 | + std::string handle; |
| 440 | + int size = 0; |
| 441 | + if (json_message.contains("params")) { |
| 442 | + const auto& params = json_message["params"]; |
| 443 | + if (params.contains("handle")) { |
| 444 | + handle = params["handle"].get<std::string>(); |
| 445 | + } |
| 446 | + if (params.contains("size")) { |
| 447 | + size = params["size"].get<int>(); |
| 448 | + } |
| 449 | + } |
| 450 | + |
| 451 | + if (method == "IO.read") { |
| 452 | + this->HandleIORead(json_message["id"].get<int>(), handle, size); |
| 453 | + } else { |
| 454 | + this->HandleIOClose(json_message["id"].get<int>(), handle); |
| 455 | + } |
| 456 | + return; |
| 457 | + } |
| 458 | + |
343 | 459 | // parse incoming message as JSON |
344 | 460 | Local<Value> arg; |
345 | 461 | success = v8::JSON::Parse(context, tns::ToV8String(isolate, message)).ToLocal(&arg); |
@@ -394,6 +510,109 @@ StringView Make8BitStringView(const std::string& value) { |
394 | 510 | isolate->PerformMicrotaskCheckpoint(); |
395 | 511 | } |
396 | 512 |
|
| 513 | +void JsV8InspectorClient::HandleLoadNetworkResource(int msgId, const std::string& url) { |
| 514 | + std::string path; |
| 515 | + if (url.rfind(kSourceMapScheme, 0) == 0) { |
| 516 | + path = url.substr(strlen(kSourceMapScheme)); |
| 517 | + } else if (url.rfind("file://", 0) == 0) { |
| 518 | + path = url.substr(strlen("file://")); |
| 519 | + } else { |
| 520 | + // Reply with a protocol error (not success:false) so DevTools falls back |
| 521 | + // to loading the resource from the frontend host, which is the |
| 522 | + // pre-existing behavior for http(s) urls. |
| 523 | + json error = {{"id", msgId}, |
| 524 | + {"error", {{"code", -32000}, {"message", "Unsupported URL scheme"}}}}; |
| 525 | + this->notify(error.dump()); |
| 526 | + return; |
| 527 | + } |
| 528 | + |
| 529 | + std::string content; |
| 530 | + bool loaded = false; |
| 531 | + |
| 532 | + if (!path.empty()) { |
| 533 | + @autoreleasepool { |
| 534 | + NSString* urlPath = [NSString stringWithUTF8String:path.c_str()]; |
| 535 | + if (urlPath != nil) { |
| 536 | + // Script urls are built by stripping RuntimeConfig.BaseDir (see |
| 537 | + // ModuleInternal::LoadClassicScript), so map the url path back to |
| 538 | + // disk; fall back to the raw path for absolute urls, and to |
| 539 | + // percent-decoded variants for urls the frontend encoded. |
| 540 | + NSString* basePath = [NSString stringWithUTF8String:RuntimeConfig.BaseDir.c_str()]; |
| 541 | + NSMutableArray<NSString*>* candidates = [NSMutableArray new]; |
| 542 | + [candidates addObject:[basePath stringByAppendingPathComponent:urlPath]]; |
| 543 | + [candidates addObject:urlPath]; |
| 544 | + NSString* decoded = [urlPath stringByRemovingPercentEncoding]; |
| 545 | + if (decoded != nil && ![decoded isEqualToString:urlPath]) { |
| 546 | + [candidates addObject:[basePath stringByAppendingPathComponent:decoded]]; |
| 547 | + [candidates addObject:decoded]; |
| 548 | + } |
| 549 | + |
| 550 | + for (NSString* candidate in candidates) { |
| 551 | + NSData* data = [NSData dataWithContentsOfFile:candidate]; |
| 552 | + if (data != nil) { |
| 553 | + content.assign(static_cast<const char*>(data.bytes), data.length); |
| 554 | + loaded = true; |
| 555 | + break; |
| 556 | + } |
| 557 | + } |
| 558 | + } |
| 559 | + } |
| 560 | + } |
| 561 | + |
| 562 | + json resource; |
| 563 | + if (loaded) { |
| 564 | + std::string handle = "ns-network-resource-" + std::to_string(++lastStreamId_); |
| 565 | + resourceStreams_[handle] = {std::move(content), 0}; |
| 566 | + resource = {{"success", true}, {"httpStatusCode", 200}, {"stream", handle}}; |
| 567 | + } else { |
| 568 | + resource = {{"success", false}, |
| 569 | + {"netError", -6}, |
| 570 | + {"netErrorName", "net::ERR_FILE_NOT_FOUND"}, |
| 571 | + {"httpStatusCode", 404}}; |
| 572 | + } |
| 573 | + |
| 574 | + json response = {{"id", msgId}, {"result", {{"resource", resource}}}}; |
| 575 | + this->notify(response.dump()); |
| 576 | +} |
| 577 | + |
| 578 | +void JsV8InspectorClient::HandleIORead(int msgId, const std::string& handle, int size) { |
| 579 | + auto it = resourceStreams_.find(handle); |
| 580 | + if (it == resourceStreams_.end()) { |
| 581 | + json error = {{"id", msgId}, |
| 582 | + {"error", {{"code", -32602}, {"message", "Invalid stream handle"}}}}; |
| 583 | + this->notify(error.dump()); |
| 584 | + return; |
| 585 | + } |
| 586 | + |
| 587 | + ResourceStream& stream = it->second; |
| 588 | + constexpr size_t kDefaultChunkSize = 1024 * 1024; |
| 589 | + size_t chunkSize = size > 0 ? static_cast<size_t>(size) : kDefaultChunkSize; |
| 590 | + size_t remaining = stream.data.size() - stream.offset; |
| 591 | + chunkSize = std::min(chunkSize, remaining); |
| 592 | + |
| 593 | + json result; |
| 594 | + if (chunkSize == 0) { |
| 595 | + // DevTools ignores any data sent alongside eof, so only signal it once |
| 596 | + // the whole stream has been delivered. |
| 597 | + result = {{"data", ""}, {"eof", true}, {"base64Encoded", false}}; |
| 598 | + } else { |
| 599 | + // Base64 keeps arbitrary file bytes intact through the JSON transport. |
| 600 | + NSData* chunk = [NSData dataWithBytes:stream.data.data() + stream.offset length:chunkSize]; |
| 601 | + NSString* encoded = [chunk base64EncodedStringWithOptions:0]; |
| 602 | + stream.offset += chunkSize; |
| 603 | + result = {{"data", [encoded UTF8String]}, {"eof", false}, {"base64Encoded", true}}; |
| 604 | + } |
| 605 | + |
| 606 | + json response = {{"id", msgId}, {"result", result}}; |
| 607 | + this->notify(response.dump()); |
| 608 | +} |
| 609 | + |
| 610 | +void JsV8InspectorClient::HandleIOClose(int msgId, const std::string& handle) { |
| 611 | + resourceStreams_.erase(handle); |
| 612 | + json response = {{"id", msgId}, {"result", json::object()}}; |
| 613 | + this->notify(response.dump()); |
| 614 | +} |
| 615 | + |
397 | 616 | Local<Context> JsV8InspectorClient::ensureDefaultContextInGroup(int contextGroupId) { |
398 | 617 | return context_.Get(isolate_); |
399 | 618 | } |
|
0 commit comments