diff --git a/a.js b/a.js new file mode 100644 index 000000000..b84b9381e --- /dev/null +++ b/a.js @@ -0,0 +1,12 @@ +export function a() { + return "a"; +} +function spawn() { + const b = new Deno.Command(); + b.spawn(); +} +function runSpawn() { + spawn(); +} +runSpawn(); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImZpbGU6Ly8vYS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLFNBQVM7RUFDZCxPQUFPO0FBQ1Q7QUFFQSxTQUFTO0VBQ1AsTUFBTSxJQUFJLElBQUksS0FBSyxPQUFPO0VBRTFCLEVBQUUsS0FBSztBQUNUO0FBUUEsU0FBUztFQUNQO0FBQ0Y7QUFFQSJ9 \ No newline at end of file diff --git a/a.js.map b/a.js.map new file mode 100644 index 000000000..e69de29bb diff --git a/bar.js b/bar.js new file mode 100644 index 000000000..3e6128c6c --- /dev/null +++ b/bar.js @@ -0,0 +1,6 @@ +"use strict"; + +throw new Error("Hello world!"); +//# sourceMappingURL=bar.js.map + +asd; diff --git a/bar.js.map b/bar.js.map new file mode 100644 index 000000000..16b12f346 --- /dev/null +++ b/bar.js.map @@ -0,0 +1,11 @@ +{ + "version": 3, + "file": "", + "sourceRoot": "", + "sources": ["http://localhost:4545/run/inline_js_source_map_2.ts"], + "sourcesContent": [ + "1+1;\ninterface Test {\n hello: string;\n}\n\nthrow new Error(\"Hello world!\" as unknown as string);\n" + ], + "names": [], + "mappings": ";AAAA,CAAC,GAAC,CAAC,CAAC;AAKJ,MAAM,IAAI,KAAK,CAAC,cAA+B,CAAC,CAAC" +} diff --git a/core/error.rs b/core/error.rs index 272819ca4..0f5c4c47b 100644 --- a/core/error.rs +++ b/core/error.rs @@ -405,12 +405,14 @@ pub struct JsStackFrame { /// Applies source map to the given location fn apply_source_map<'a>( + scope: &mut v8::HandleScope, source_mapper: &mut crate::source_map::SourceMapper, file_name: Cow<'a, str>, line_number: i64, column_number: i64, ) -> (Cow<'a, str>, i64, i64) { match source_mapper.apply_source_map( + scope, &file_name, line_number as u32, column_number as u32, @@ -548,7 +550,7 @@ impl JsStackFrame { ) { (Some(f), Some(l), Some(c)) => { let (file_name, line_num, col_num) = - apply_source_map(&mut source_mapper, f.into(), l, c); + apply_source_map(scope, &mut source_mapper, f.into(), l, c); (Some(file_name.into_owned()), Some(line_num), Some(col_num)) } (f, l, c) => (f, l, c), @@ -561,7 +563,7 @@ impl JsStackFrame { return Some(o); }; let (file, line, col) = - apply_source_map(&mut source_mapper, file.into(), line, col); + apply_source_map(scope, &mut source_mapper, file.into(), line, col); Some(format!("{before}{file}:{line}:{col}{after}")) }); @@ -599,7 +601,7 @@ impl JsStackFrame { let state = JsRuntime::state_from(scope); let mut source_mapper = state.source_mapper.borrow_mut(); let (file_name, line_num, col_num) = - apply_source_map(&mut source_mapper, f.into(), l, c); + apply_source_map(scope, &mut source_mapper, f.into(), l, c); Some(JsStackFrame::from_location( Some(file_name.into_owned()), Some(line_num), @@ -706,7 +708,7 @@ impl JsError { Self::inner_from_v8_exception(scope, exception, Default::default()) } - pub fn from_v8_message<'a>( + pub(crate) fn from_v8_message<'a>( scope: &'a mut v8::HandleScope, msg: v8::Local<'a, v8::Message>, ) -> Self { @@ -716,42 +718,21 @@ impl JsError { let exception_message = msg.get(scope).to_rust_string_lossy(scope); - // Convert them into Vec - let mut frames: Vec = vec![]; - let mut source_line = None; - let mut source_line_frame_index = None; - - if let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg) { - frames = vec![stack_frame]; - } - { - let state = JsRuntime::state_from(scope); - let mut source_mapper = state.source_mapper.borrow_mut(); - for (i, frame) in frames.iter().enumerate() { - if let (Some(file_name), Some(line_number)) = - (&frame.file_name, frame.line_number) - { - if !file_name.trim_start_matches('[').starts_with("ext:") { - source_line = source_mapper.get_source_line(file_name, line_number); - source_line_frame_index = Some(i); - break; - } - } - } - } - - Self { + let mut js_error = Self { name: None, message: None, exception_message, cause: None, - source_line, - source_line_frame_index, - frames, + source_line: None, + source_line_frame_index: None, + frames: vec![], stack: None, aggregated: None, additional_properties: vec![], } + + js_error.frames = vec![stack_frame]; + js_error } fn inner_from_v8_exception<'a>( @@ -874,14 +855,14 @@ impl JsError { { let state = JsRuntime::state_from(scope); let mut source_mapper = state.source_mapper.borrow_mut(); - for (i, frame) in frames.iter().enumerate() { + for (index, frame) in frames.iter().enumerate() { if let (Some(file_name), Some(line_number)) = (&frame.file_name, frame.line_number) { if !file_name.trim_start_matches('[').starts_with("ext:") { source_line = source_mapper.get_source_line(file_name, line_number); - source_line_frame_index = Some(i); + source_line_frame_index = Some(index); break; } } @@ -973,13 +954,35 @@ impl std::error::Error for JsError {} impl Display for JsError { fn fmt(&self, f: &mut Formatter) -> fmt::Result { + // eprintln!("inside js error {}", self.source_line.is_some()); if let Some(stack) = &self.stack { + // eprintln!("inside js error stack"); let stack_lines = stack.lines(); if stack_lines.count() > 1 { return write!(f, "{stack}"); } } + write!(f, "{}", self.exception_message)?; + if let Some(source_line) = self.source_line.as_ref() { + writeln!(f)?; + write!(f, " {}", source_line)?; + let column_number = self + .source_line_frame_index + .and_then(|i| self.frames.get(i).unwrap().column_number); + if let Some(column_number) = column_number { + let mut s = String::new(); + for _i in 0..(column_number - 1) { + if source_line.chars().nth(_i as usize).unwrap() == '\t' { + s.push('\t'); + } else { + s.push(' '); + } + } + s.push('^'); + write!(f, " {}", s)?; + } + } let location = self.frames.first().and_then(|f| f.maybe_format_location()); if let Some(location) = location { write!(f, "\n at {location}")?; diff --git a/core/examples/ts_module_loader.rs b/core/examples/ts_module_loader.rs index 83a509c92..2024e813d 100644 --- a/core/examples/ts_module_loader.rs +++ b/core/examples/ts_module_loader.rs @@ -133,6 +133,18 @@ impl ModuleLoader for TypescriptModuleLoader { ModuleLoadResponse::Sync(load(source_maps, module_specifier)) } + fn get_source_map_for_file(&self, file_name: &str) -> Option> { + self.source_maps.borrow().get(file_name).cloned() + } + + fn load_source_map_file( + &self, + source_map_file_name: &str, + _file_name: &str, + ) -> Option> { + std::fs::read(source_map_file_name).ok() + } + fn get_source_map(&self, specifier: &str) -> Option> { self .source_maps diff --git a/core/modules/loaders.rs b/core/modules/loaders.rs index b303f2979..c56887b77 100644 --- a/core/modules/loaders.rs +++ b/core/modules/loaders.rs @@ -134,6 +134,32 @@ pub trait ModuleLoader { None } + /// Some embedders might opt into stripping out inlined source map from the source + /// code before it's loaded into V8. The source mapping logic will first ask + /// embedder to provide a source map for given file, before falling back + /// to looking for source map inlined in the code. + fn get_source_map_for_file(&self, _file_name: &str) -> Option> {} + + /// Returns a source map file. + /// + /// This is fallback API for [`Self::get_source_map_for_file`] that will only be called + /// if a module contains magic comment like `//# sourceMappingURL=foo.js.map`. + /// + /// Embedders might apply additional logic to decide if the source map should be returned. + /// Eg. if a local file tries to link to a remote source map, embedder might opt into + /// refusing to load it. + fn load_source_map_file( + &self, + _source_map_file_name: &str, + _file_name: &str, + ) -> Option> { + None + } + + /// Return an "original" version of the given line for a file. + /// + /// In the future this API might be deprecated in favor of handling it internally + /// in `deno_core` using contents of the source map. fn get_source_mapped_source_line( &self, _file_name: &str, diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs index 4d07db3fe..e91a18674 100644 --- a/core/ops_builtin_v8.rs +++ b/core/ops_builtin_v8.rs @@ -1155,6 +1155,12 @@ pub fn op_current_user_call_site( } let line_number = frame.get_line_number() as u32; let column_number = frame.get_column() as u32; + // TODO(bartlomieju): + // let application = js_runtime_state + // .source_mapper + // .borrow_mut() + // .apply_source_map(scope, &file_name, line_number, column_number); + let (file_name, application) = match frame.get_script_name(scope) { Some(name) => { let file_name = name.to_rust_string_lossy(scope); diff --git a/core/source_map.rs b/core/source_map.rs index de8b82d4e..b0a4094ae 100644 --- a/core/source_map.rs +++ b/core/source_map.rs @@ -3,14 +3,24 @@ //! This mod provides functions to remap a `JsError` based on a source map. use crate::ModuleLoader; +use crate::ModuleLoader; +use crate::ModuleName; use crate::ModuleName; +use crate::ModuleResolutionError; +use crate::RequestedModuleType; +use crate::resolve_import; +use crate::resolve_url; use crate::resolve_url; +use crate::runtime::JsRealm; +use sourcemap::DecodedMap; pub use sourcemap::SourceMap; use std::borrow::Cow; use std::collections::HashMap; use std::rc::Rc; use std::str; +const BASE64_PREFIX: &str = "data:application/json;base64,"; + #[derive(Debug, PartialEq)] pub enum SourceMapApplication { /// No mapping was applied, the location is unchanged. @@ -66,6 +76,71 @@ 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 { + 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); + + if !maybe_source_mapping_url.is_string() { + return None; + } + + let source_map_string = + maybe_source_mapping_url.to_rust_string_lossy(scope); + + // 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 source_map_string.starts_with(BASE64_PREFIX) { + let DecodedMap::Regular(sm) = + sourcemap::decode_data_url(&source_map_string).ok()? + else { + return None; + }; + sm + } 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, + }, + }; + let url = url?; + if url.scheme() != "file" { + return None; + } + let source_map_file_name = url.to_file_path().ok()?; + let source_map_file_name = source_map_file_name.to_str()?; + let contents = module_map_rc + .loader + .borrow() + .load_source_map_file(source_map_file_name, file_name)?; + 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. @@ -73,6 +148,7 @@ impl SourceMapper { /// 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, @@ -88,14 +164,42 @@ impl SourceMapper { SourceMap::from_slice(self.ext_source_maps.get(file_name)?).ok() }) .or_else(|| { - SourceMap::from_slice(&self.loader.get_source_map(file_name)?).ok() + SourceMap::from_slice( + &self.loader.get_source_map_for_file(file_name)?, + ) + .ok() }) }); - let Some(source_map) = maybe_source_map.as_ref() else { - return SourceMapApplication::Unchanged; + // If source map is provided externally, return early + if let Some(source_map) = maybe_source_map.as_ref() { + return Self::compute_application( + source_map, + file_name, + line_number, + column_number, + ); }; + // Finally fallback to using V8 APIs to discover source map inside the source code of the module. + if let Some(app) = self.apply_source_map_from_module_map( + scope, + file_name, + line_number, + column_number, + ) { + return app; + } + + SourceMapApplication::Unchanged + } + + 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; @@ -143,6 +247,7 @@ impl SourceMapper { file_name: &str, line_number: i64, ) -> Option { + // eprintln!("get_source_line {} {}", file_name, line_number); if let Some(maybe_source_line) = self.source_lines.get(&(file_name.to_string(), line_number)) { @@ -168,11 +273,14 @@ mod tests { use url::Url; use super::*; + use crate::JsRuntime; use crate::ModuleCodeString; use crate::ModuleLoadResponse; use crate::ModuleSpecifier; use crate::RequestedModuleType; use crate::ResolutionKind; + use crate::RuntimeOptions; + use crate::ascii_str; use crate::ascii_str; use crate::error::ModuleLoaderError; @@ -205,6 +313,15 @@ mod tests { unreachable!() } + fn get_source_map_for_file(&self, file_name: &str) -> Option> { + let url = Url::parse(file_name).unwrap(); + let content = self.map.get(&url)?; + content + .source_map + .as_ref() + .map(|s| Cow::Borrowed(s.as_bytes())) + } + fn get_source_map(&self, file_name: &str) -> Option> { let url = Url::parse(file_name).unwrap(); let content = self.map.get(&url)?; @@ -214,6 +331,14 @@ mod tests { .map(|s| Cow::Borrowed(s.as_bytes())) } + fn load_source_map_file( + &self, + _source_map_file_name: &str, + _file_name: &str, + ) -> Option> { + todo!() + } + fn get_source_mapped_source_line( &self, _file_name: &str, @@ -237,19 +362,29 @@ mod tests { }, ); - let mut source_mapper = SourceMapper::new(Rc::new(loader)); + 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 { diff --git a/fizz.ts b/fizz.ts new file mode 100644 index 000000000..1a34dfefa --- /dev/null +++ b/fizz.ts @@ -0,0 +1,13 @@ +interface Foobar { + a: string; + b: number; + c: boolean; +} + +console.log("hello there"); + +function boom(): never { + throw new Error("boom" as string); +} + +boom(); diff --git a/foo.js b/foo.js new file mode 100644 index 000000000..ab149db68 --- /dev/null +++ b/foo.js @@ -0,0 +1,6 @@ +"use strict"; + +throw new Error("Hello world!"); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiaHR0cDovL2xvY2FsaG9zdDo0NTQ1L3J1bi9pbmxpbmVfanNfc291cmNlX21hcF8yLnRzIl0sInNvdXJjZXNDb250ZW50IjpbIjErMTtcbmludGVyZmFjZSBUZXN0IHtcbiAgaGVsbG86IHN0cmluZztcbn1cblxudGhyb3cgbmV3IEVycm9yKFwiSGVsbG8gd29ybGQhXCIgYXMgdW5rbm93biBhcyBzdHJpbmcpO1xuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQSxDQUFDLEdBQUMsQ0FBQyxDQUFDO0FBS0osTUFBTSxJQUFJLEtBQUssQ0FBQyxjQUErQixDQUFDLENBQUMifQ== + +asd; diff --git a/testing/checkin/runner/extensions.rs b/testing/checkin/runner/extensions.rs index f94ba9463..070d2719e 100644 --- a/testing/checkin/runner/extensions.rs +++ b/testing/checkin/runner/extensions.rs @@ -6,7 +6,9 @@ use crate::checkin::runner::ops; use crate::checkin::runner::ops_async; use crate::checkin::runner::ops_buffer; use crate::checkin::runner::ops_error; +use crate::checkin::runner::ops_fs; use crate::checkin::runner::ops_io; +use crate::checkin::runner::ops_transpiler; use crate::checkin::runner::ops_worker; pub trait SomeType {} @@ -41,6 +43,10 @@ deno_core::extension!( ops_error::op_async_throw_error_lazy, ops_error::op_async_throw_error_deferred, ops_error::op_error_custom_sync, + ops_error::op_error_context_sync, + ops_error::op_error_context_async, + ops_fs::op_fs_read_text_file, + ops_fs::op_fs_write_text_file, ops_error::op_error_custom_with_code_sync, ops_buffer::op_v8slice_store, ops_buffer::op_v8slice_clone, @@ -50,6 +56,7 @@ deno_core::extension!( ops_worker::op_worker_parent, ops_worker::op_worker_await_close, ops_worker::op_worker_terminate, + ops_transpiler::op_transpile, ], objects = [ ops::DOMPointReadOnly, @@ -65,9 +72,11 @@ deno_core::extension!( "checkin:console" = "console.ts", "checkin:object" = "object.ts", "checkin:error" = "error.ts", + "checkin:fs" = "fs.ts", + "checkin:throw" = "throw.ts", "checkin:timers" = "timers.ts", + "checkin:transpiler" = "transpiler.ts", "checkin:worker" = "worker.ts", - "checkin:throw" = "throw.ts", "checkin:callsite" = "callsite.ts", ], state = |state| { diff --git a/testing/checkin/runner/mod.rs b/testing/checkin/runner/mod.rs index 32f4f6674..bb6a28443 100644 --- a/testing/checkin/runner/mod.rs +++ b/testing/checkin/runner/mod.rs @@ -25,7 +25,9 @@ mod ops; mod ops_async; mod ops_buffer; mod ops_error; +mod ops_fs; mod ops_io; +mod ops_transpiler; mod ops_worker; pub mod snapshot; #[cfg(test)] diff --git a/testing/checkin/runner/ops_fs.rs b/testing/checkin/runner/ops_fs.rs new file mode 100644 index 000000000..9adf5dfda --- /dev/null +++ b/testing/checkin/runner/ops_fs.rs @@ -0,0 +1,22 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::AnyError; +use deno_core::op2; + +#[op2] +#[string] +pub fn op_fs_read_text_file( + #[string] file_path: &str, +) -> Result { + let content = std::fs::read_to_string(file_path)?; + Ok(content) +} + +#[op2(fast)] +pub fn op_fs_write_text_file( + #[string] file_path: &str, + #[string] content: &str, +) -> Result<(), AnyError> { + std::fs::write(file_path, content)?; + Ok(()) +} diff --git a/testing/checkin/runner/ops_transpiler.rs b/testing/checkin/runner/ops_transpiler.rs new file mode 100644 index 000000000..d36fcbcf9 --- /dev/null +++ b/testing/checkin/runner/ops_transpiler.rs @@ -0,0 +1,46 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_ast::MediaType; +use deno_ast::ParseParams; +use deno_ast::SourceMapOption; +use deno_core::error::AnyError; +use deno_core::op2; +use deno_core::url::Url; + +#[op2] +#[serde] +pub fn op_transpile( + #[string] specifier: &str, + #[string] source: String, +) -> Result<(String, Option), AnyError> { + let media_type = MediaType::from_str(specifier); + + let parsed = deno_ast::parse_module(ParseParams { + specifier: Url::parse(specifier).unwrap(), + text: source.into(), + media_type, + capture_tokens: false, + scope_analysis: false, + maybe_syntax: None, + })?; + let transpile_result = parsed.transpile( + &deno_ast::TranspileOptions { + use_decorators_proposal: true, + imports_not_used_as_values: deno_ast::ImportsNotUsedAsValues::Remove, + ..Default::default() + }, + &deno_ast::EmitOptions { + source_map: SourceMapOption::Inline, + inline_sources: false, + ..Default::default() + }, + )?; + let transpiled_source = transpile_result.into_source(); + + Ok(( + String::from_utf8(transpiled_source.source).unwrap(), + transpiled_source + .source_map + .map(|s| String::from_utf8(s).unwrap()), + )) +} diff --git a/testing/checkin/runner/ts_module_loader.rs b/testing/checkin/runner/ts_module_loader.rs index b776a505b..e72015275 100644 --- a/testing/checkin/runner/ts_module_loader.rs +++ b/testing/checkin/runner/ts_module_loader.rs @@ -188,6 +188,41 @@ impl ModuleLoader for TypescriptModuleLoader { )) } + fn get_source_map_for_file(&self, file_name: &str) -> Option> { + self.source_maps.borrow().get(file_name).cloned() + } + + fn load_source_map_file( + &self, + source_map_file_name: &str, + _file_name: &str, + ) -> Option> { + std::fs::read(source_map_file_name).ok() + } + + fn get_source_mapped_source_line( + &self, + file_name: &str, + line_number: usize, + ) -> Option { + let url = Url::parse(file_name).ok()?; + eprintln!("get_source_mapped_source_line {}", url.as_str()); + if url.scheme() != "file" { + return None; + } + let path = url.to_file_path().unwrap(); + let code = std::fs::read_to_string(path).ok()?; + eprintln!("code {}", code); + // Do NOT use .lines(): it skips the terminating empty line. + // (due to internally using_terminator() instead of .split()) + let lines: Vec<&str> = code.split('\n').collect(); + if line_number >= lines.len() { + None + } else { + Some(lines[line_number].to_string()) + } + } + fn get_source_map(&self, specifier: &str) -> Option> { self .source_maps diff --git a/testing/checkin/runtime/__init.js b/testing/checkin/runtime/__init.js index 1a82834ed..a52985662 100644 --- a/testing/checkin/runtime/__init.js +++ b/testing/checkin/runtime/__init.js @@ -2,8 +2,10 @@ import * as async from "checkin:async"; import * as console from "checkin:console"; import * as error from "checkin:error"; +import * as fs from "checkin:fs"; import * as timers from "checkin:timers"; import * as worker from "checkin:worker"; +import * as transpiler from "checkin:transpiler"; import * as throw_ from "checkin:throw"; import * as object from "checkin:object"; import * as callsite from "checkin:callsite"; @@ -19,6 +21,9 @@ globalThis.setInterval = timers.setInterval; globalThis.clearTimeout = timers.clearTimeout; globalThis.clearInterval = timers.clearInterval; globalThis.Worker = worker.Worker; +globalThis.Transpiler = transpiler.Transpiler; +globalThis.readTextFile = fs.readTextFile; +globalThis.writeTextFile = fs.writeTextFile; Deno.core.addMainModuleHandler((module) => { if (onMainModuleCb) onMainModuleCb(module); }); diff --git a/testing/checkin/runtime/fs.ts b/testing/checkin/runtime/fs.ts new file mode 100644 index 000000000..5fd2a84b2 --- /dev/null +++ b/testing/checkin/runtime/fs.ts @@ -0,0 +1,10 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { op_fs_read_text_file, op_fs_write_text_file } from "ext:core/ops"; + +export function readTextFile(filePath: string): string { + return op_fs_read_text_file(filePath); +} + +export function writeTextFile(filePath: string, content: string) { + op_fs_write_text_file(filePath, content); +} diff --git a/testing/checkin/runtime/transpiler.ts b/testing/checkin/runtime/transpiler.ts new file mode 100644 index 000000000..4d8728d11 --- /dev/null +++ b/testing/checkin/runtime/transpiler.ts @@ -0,0 +1,11 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { op_transpile } from "ext:core/ops"; + +export class Transpiler { + constructor() { + } + + transpile(specifier: string, source: string) { + return op_transpile(specifier, source); + } +} diff --git a/testing/ops.d.ts b/testing/ops.d.ts index 8d4f44992..5fb5ac403 100644 --- a/testing/ops.d.ts +++ b/testing/ops.d.ts @@ -12,6 +12,12 @@ export function op_async_throw_error_lazy(...any: any[]): any; export function op_error_context_async(...any: any[]): any; export function op_error_context_sync(...any: any[]): any; export function op_error_custom_sync(...any: any[]): any; +export function op_fs_read_text_file(path: string): string; +export function op_fs_write_text_file(path: string, content: string): void; +export function op_transpile( + specifier: string, + source: string, +): [string, string | undefined]; export function op_error_custom_with_code_sync(...any: any[]): any; export function op_worker_await_close(...any: any[]): any; diff --git a/testing/tsconfig.json b/testing/tsconfig.json index f5cd582b5..158c95963 100644 --- a/testing/tsconfig.json +++ b/testing/tsconfig.json @@ -8,9 +8,11 @@ "checkin:async": ["checkin/runtime/async.ts"], "checkin:console": ["checkin/runtime/console.ts"], "checkin:error": ["checkin/runtime/error.ts"], + "checkin:fs": ["checkin/runtime/fs.ts"], "checkin:testing": ["checkin/runtime/testing.ts"], "checkin:throw": ["checkin/runtime/throw.ts"], "checkin:timers": ["checkin/runtime/timers.ts"], + "checkin:transpiler": ["checkin/runtime/transpiler.ts"], "checkin:worker": ["checkin/runtime/worker.ts"], "checkin:object": ["checkin/runtime/object.ts"], "checkin:callsite": ["checkin/runtime/callsite.ts"] diff --git a/transpile.js b/transpile.js new file mode 100644 index 000000000..8dcb63b38 --- /dev/null +++ b/transpile.js @@ -0,0 +1,30 @@ +const transpiler = new Transpiler(); + +const SOURCE = `export function a(): string { + return "a"; +} + +function spawn() { + const b = new Deno.Command(); + + b.spawn(); +} + +interface Foobar { + a: string; + b: number; + c: boolean; +} + +function runSpawn() { + spawn(); +} + +runSpawn();`; + +const SPECIFIER = "file:///a.ts"; +const TRANSPILED_SOURCE_FILE_PATH = "./a.js"; +const SOURCE_MAP_FILE_PATH = "./a.js.map"; +const [transpiledSource, sourceMap] = transpiler.transpile(SPECIFIER, SOURCE); +writeTextFile(TRANSPILED_SOURCE_FILE_PATH, transpiledSource); +writeTextFile(SOURCE_MAP_FILE_PATH, sourceMap);