- 
                Notifications
    
You must be signed in to change notification settings  - Fork 132
 
[WIP] feat: Add native SourceMap support #819
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 20 commits
3d8c9c8
              2905e0d
              23cdef9
              ec8e4f1
              333c579
              72ffd9d
              588dc8a
              a78caa2
              a46cd55
              0a2821c
              6fc5d1d
              0166243
              6bfb77b
              224bcf2
              01bf21c
              7ff9928
              40aff1a
              84d6c3f
              7a380af
              85a9634
              b0bfb5c
              a6c6f76
              126eb6b
              4e4036d
              b253371
              a503d80
              cc573b4
              f01fbe8
              2d4fdb9
              1ccf6c6
              0795341
              f764040
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| "use strict"; | ||
| 
     | 
||
| throw new Error("Hello world!"); | ||
| //# sourceMappingURL=bar.js.map | ||
| 
     | 
||
| asd; | 
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| 
          
            
          
           | 
    @@ -5,9 +5,15 @@ | |
| // TODO(bartlomieju): remove once `SourceMapGetter` is removed. | ||
| #![allow(deprecated)] | ||
| 
     | 
||
| use crate::module_specifier::ModuleResolutionError; | ||
| use crate::resolve_import; | ||
| use crate::resolve_url; | ||
| use crate::runtime::JsRealm; | ||
| use crate::ModuleLoader; | ||
| use crate::ModuleName; | ||
| use crate::RequestedModuleType; | ||
| use base64::prelude::BASE64_STANDARD; | ||
| use base64::Engine; | ||
| pub use sourcemap::SourceMap; | ||
| use std::borrow::Cow; | ||
| use std::collections::HashMap; | ||
| 
          
            
          
           | 
    @@ -89,13 +95,93 @@ impl SourceMapper { | |
| std::mem::take(&mut self.ext_source_maps) | ||
| } | ||
| 
     | 
||
| pub fn apply_source_map_from_module_map( | ||
| &mut self, | ||
| scope: &mut v8::HandleScope, | ||
| file_name: &str, | ||
| line_number: u32, | ||
| column_number: u32, | ||
| ) -> Option<SourceMapApplication> { | ||
| let module_map_rc = JsRealm::module_map_from(scope); | ||
| let id = module_map_rc.get_id(file_name, RequestedModuleType::None)?; | ||
| 
     | 
||
| let module_handle = module_map_rc.get_handle(id).unwrap(); | ||
| let module = v8::Local::new(scope, module_handle); | ||
| let unbound_module_script = module.get_unbound_module_script(scope); | ||
| let maybe_source_mapping_url = | ||
| unbound_module_script.get_source_mapping_url(scope); | ||
| 
     | 
||
| // TODO(bartlomieju): This should be the last fallback and it's only useful | ||
| // for eval - probably for `Deno.core.evalContext()`. | ||
| // let maybe_source_url = unbound_module_script | ||
| // .get_source_url(scope) | ||
| // .to_rust_string_lossy(scope); | ||
| // eprintln!("maybe source url {}", maybe_source_url); | ||
| 
     | 
||
| if !maybe_source_mapping_url.is_string() { | ||
| return None; | ||
| } | ||
                
      
                  bartlomieju marked this conversation as resolved.
               
          
            Show resolved
            Hide resolved
         | 
||
| 
     | 
||
| let source_map_string = | ||
| maybe_source_mapping_url.to_rust_string_lossy(scope); | ||
| // eprintln!("maybe source mapping url {}", source_map_string); | ||
| 
     | 
||
| // TODO(bartlomieju): this is a fast path - if it fails, we should try to parse | ||
| // the URL (or resolve it from the current file being mapped) and fallback to | ||
| // acquiring a source map from that URL. In Deno we might want to apply permissions | ||
| // checks for fetching the map. | ||
| let source_map = if let Some(b64) = | ||
| source_map_string.strip_prefix("data:application/json;base64,") | ||
| { | ||
| let decoded_b64 = BASE64_STANDARD.decode(b64).ok()?; | ||
                
       | 
||
| // eprintln!( | ||
| // "source map {:?}", | ||
| // String::from_utf8(decoded_b64.clone()).unwrap() | ||
| // ); | ||
| SourceMap::from_slice(&decoded_b64).ok()? | ||
| } else { | ||
| let url = match resolve_import(&source_map_string, file_name) { | ||
| Ok(url) => Some(url), | ||
| Err(err) => match err { | ||
| ModuleResolutionError::ImportPrefixMissing(_, _) => { | ||
| resolve_import(&format!("./{}", source_map_string), file_name).ok() | ||
| } | ||
| _ => None, | ||
| }, | ||
| }; | ||
| // eprintln!( | ||
| // "source map url sourceMappingURL={} file_name={} url={}", | ||
| // source_map_string, | ||
| // file_name, | ||
| // url.as_ref().map(|s| s.as_str()).unwrap_or_default() | ||
| // ); | ||
| let url = url?; | ||
| if url.scheme() != "file" { | ||
| return None; | ||
| } | ||
| let contents = module_map_rc | ||
| .loader | ||
| .borrow() | ||
| .get_source_map(url.to_file_path().ok()?.to_str()?)?; | ||
                
      
                  bartlomieju marked this conversation as resolved.
               
              
                Outdated
          
            Show resolved
            Hide resolved
         | 
||
| SourceMap::from_slice(&contents).ok()? | ||
| }; | ||
| 
     | 
||
| Some(Self::compute_application( | ||
| &source_map, | ||
| file_name, | ||
| line_number, | ||
| column_number, | ||
| )) | ||
| } | ||
| 
     | 
||
| /// Apply a source map to the passed location. If there is no source map for | ||
| /// this location, or if the location remains unchanged after mapping, the | ||
| /// changed values are returned. | ||
| /// | ||
| /// Line and column numbers are 1-based. | ||
| pub fn apply_source_map( | ||
| &mut self, | ||
| scope: &mut v8::HandleScope, | ||
| file_name: &str, | ||
| line_number: u32, | ||
| column_number: u32, | ||
| 
        
          
        
         | 
    @@ -104,6 +190,17 @@ impl SourceMapper { | |
| let line_number = line_number - 1; | ||
| let column_number = column_number - 1; | ||
| 
     | 
||
| // TODO(bartlomieju): requires scope and should only be called in a fallback op, | ||
| // that will access scope if the fast op doesn't return anything. | ||
| if let Some(app) = self.apply_source_map_from_module_map( | ||
| scope, | ||
| file_name, | ||
| line_number, | ||
| column_number, | ||
| ) { | ||
| return app; | ||
| } | ||
                
      
                  bartlomieju marked this conversation as resolved.
               
              
                Outdated
          
            Show resolved
            Hide resolved
         | 
||
| 
     | 
||
| let getter = self.getter.as_ref(); | ||
| let maybe_source_map = | ||
| self.maps.entry(file_name.to_owned()).or_insert_with(|| { | ||
| 
        
          
        
         | 
    @@ -123,6 +220,15 @@ impl SourceMapper { | |
| return SourceMapApplication::Unchanged; | ||
| }; | ||
| 
     | 
||
| Self::compute_application(source_map, file_name, line_number, column_number) | ||
| } | ||
| 
     | 
||
| fn compute_application( | ||
| source_map: &SourceMap, | ||
| file_name: &str, | ||
| line_number: u32, | ||
| column_number: u32, | ||
| ) -> SourceMapApplication { | ||
| let Some(token) = source_map.lookup_token(line_number, column_number) | ||
| else { | ||
| return SourceMapApplication::Unchanged; | ||
| 
          
            
          
           | 
    @@ -212,11 +318,13 @@ mod tests { | |
| 
     | 
||
| use super::*; | ||
| use crate::ascii_str; | ||
| use crate::JsRuntime; | ||
| use crate::ModuleCodeString; | ||
| use crate::ModuleLoadResponse; | ||
| use crate::ModuleSpecifier; | ||
| use crate::RequestedModuleType; | ||
| use crate::ResolutionKind; | ||
| use crate::RuntimeOptions; | ||
| 
     | 
||
| struct SourceMapLoaderContent { | ||
| source_map: Option<ModuleCodeString>, | ||
| 
          
            
          
           | 
    @@ -279,19 +387,29 @@ mod tests { | |
| }, | ||
| ); | ||
| 
     | 
||
| let mut source_mapper = SourceMapper::new(Rc::new(loader), None); | ||
| let loader = Rc::new(loader); | ||
| 
     | 
||
| let mut js_runtime = JsRuntime::new(RuntimeOptions { | ||
| module_loader: Some(loader.clone()), | ||
| ..Default::default() | ||
| }); | ||
| let state = JsRuntime::state_from(js_runtime.v8_isolate()); | ||
| let scope = &mut js_runtime.handle_scope(); | ||
| let mut source_mapper = state.source_mapper.borrow_mut(); | ||
| 
     | 
||
| // Non-existent file | ||
| let application = | ||
| source_mapper.apply_source_map("file:///doesnt_exist.js", 1, 1); | ||
| source_mapper.apply_source_map(scope, "file:///doesnt_exist.js", 1, 1); | ||
| assert_eq!(application, SourceMapApplication::Unchanged); | ||
| 
     | 
||
| // File with no source map | ||
| let application = source_mapper.apply_source_map("file:///b.js", 1, 1); | ||
| let application = | ||
| source_mapper.apply_source_map(scope, "file:///b.js", 1, 1); | ||
| assert_eq!(application, SourceMapApplication::Unchanged); | ||
| 
     | 
||
| // File with a source map | ||
| let application = source_mapper.apply_source_map("file:///a.ts", 1, 21); | ||
| let application = | ||
| source_mapper.apply_source_map(scope, "file:///a.ts", 1, 21); | ||
| assert_eq!( | ||
| application, | ||
| SourceMapApplication::LineAndColumn { | ||
| 
          
            
          
           | 
    ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| "use strict"; | ||
| 
     | 
||
| throw new Error("Hello world!"); | ||
| //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiaHR0cDovL2xvY2FsaG9zdDo0NTQ1L3J1bi9pbmxpbmVfanNfc291cmNlX21hcF8yLnRzIl0sInNvdXJjZXNDb250ZW50IjpbIjErMTtcbmludGVyZmFjZSBUZXN0IHtcbiAgaGVsbG86IHN0cmluZztcbn1cblxudGhyb3cgbmV3IEVycm9yKFwiSGVsbG8gd29ybGQhXCIgYXMgdW5rbm93biBhcyBzdHJpbmcpO1xuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQSxDQUFDLEdBQUMsQ0FBQyxDQUFDO0FBS0osTUFBTSxJQUFJLEtBQUssQ0FBQyxjQUErQixDQUFDLENBQUMifQ== | ||
| 
     | 
||
| asd; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verify this in a separate PR. We support source maps for
ext:modules so most likely this is not needed