diff --git a/cli/ast.rs b/cli/ast.rs index 76a5f13629..636dc1881a 100644 --- a/cli/ast.rs +++ b/cli/ast.rs @@ -205,6 +205,9 @@ pub struct EmitOptions { /// Should the source map be inlined in the emitted code file, or provided /// as a separate file. Defaults to `true`. pub inline_source_map: bool, + // Should a corresponding .map file be created for the output. This should be + // false if inline_source_map is true. Defaults to `false`. + pub source_map: bool, /// When transforming JSX, what value should be used for the JSX factory. /// Defaults to `React.createElement`. pub jsx_factory: String, @@ -222,6 +225,7 @@ impl Default for EmitOptions { emit_metadata: false, imports_not_used_as_values: ImportsNotUsedAsValues::Remove, inline_source_map: true, + source_map: false, jsx_factory: "React.createElement".into(), jsx_fragment_factory: "React.Fragment".into(), transform_jsx: true, @@ -244,6 +248,7 @@ impl From for EmitOptions { emit_metadata: options.emit_decorator_metadata, imports_not_used_as_values, inline_source_map: options.inline_source_map, + source_map: options.source_map, jsx_factory: options.jsx_factory, jsx_fragment_factory: options.jsx_fragment_factory, transform_jsx: options.jsx == "react", diff --git a/cli/config_file.rs b/cli/config_file.rs index a7fe7f2dec..1875f9906d 100644 --- a/cli/config_file.rs +++ b/cli/config_file.rs @@ -24,6 +24,7 @@ pub struct EmitConfigOptions { pub emit_decorator_metadata: bool, pub imports_not_used_as_values: String, pub inline_source_map: bool, + pub source_map: bool, pub jsx: String, pub jsx_factory: String, pub jsx_fragment_factory: String, diff --git a/cli/module_graph.rs b/cli/module_graph.rs index 1ca738977f..8613be1e7b 100644 --- a/cli/module_graph.rs +++ b/cli/module_graph.rs @@ -769,7 +769,8 @@ impl Graph { "checkJs": false, "emitDecoratorMetadata": false, "importsNotUsedAsValues": "remove", - "inlineSourceMap": true, + "inlineSourceMap": false, + "sourceMap": false, "jsx": "react", "jsxFactory": "React.createElement", "jsxFragmentFactory": "React.Fragment", @@ -777,7 +778,7 @@ impl Graph { let maybe_ignored_options = ts_config .merge_tsconfig_from_config_file(options.maybe_config_file.as_ref())?; - let s = self.emit_bundle( + let (src, _) = self.emit_bundle( &root_specifier, &ts_config.into(), &BundleType::Module, @@ -787,7 +788,7 @@ impl Graph { ("Total time".to_string(), start.elapsed().as_millis() as u32), ]); - Ok((s, stats, maybe_ignored_options)) + Ok((src, stats, maybe_ignored_options)) } /// Type check the module graph, corresponding to the options provided. @@ -945,6 +946,7 @@ impl Graph { "experimentalDecorators": true, "importsNotUsedAsValues": "remove", "inlineSourceMap": false, + "sourceMap": false, "isolatedModules": true, "jsx": "react", "jsxFactory": "React.createElement", @@ -958,6 +960,8 @@ impl Graph { let opts = match options.bundle_type { BundleType::Module | BundleType::Classic => json!({ "noEmit": true, + "removeComments": true, + "sourceMap": true, }), BundleType::None => json!({ "outDir": "deno://", @@ -1008,12 +1012,15 @@ impl Graph { "Only a single root module supported." ); let specifier = &graph.roots[0]; - let s = graph.emit_bundle( + let (src, maybe_src_map) = graph.emit_bundle( specifier, &config.into(), &options.bundle_type, )?; - emitted_files.insert("deno:///bundle.js".to_string(), s); + emitted_files.insert("deno:///bundle.js".to_string(), src); + if let Some(src_map) = maybe_src_map { + emitted_files.insert("deno:///bundle.js.map".to_string(), src_map); + } } BundleType::None => { for emitted_file in &response.emitted_files { @@ -1060,13 +1067,16 @@ impl Graph { "Only a single root module supported." ); let specifier = &self.roots[0]; - let s = self.emit_bundle( + let (src, maybe_src_map) = self.emit_bundle( specifier, &config.into(), &options.bundle_type, )?; emit_count += 1; - emitted_files.insert("deno:///bundle.js".to_string(), s); + emitted_files.insert("deno:///bundle.js".to_string(), src); + if let Some(src_map) = maybe_src_map { + emitted_files.insert("deno:///bundle.js.map".to_string(), src_map); + } } BundleType::None => { let emit_options: ast::EmitOptions = config.into(); @@ -1118,7 +1128,7 @@ impl Graph { specifier: &ModuleSpecifier, emit_options: &ast::EmitOptions, bundle_type: &BundleType, - ) -> Result { + ) -> Result<(String, Option), AnyError> { let cm = Rc::new(swc_common::SourceMap::new( swc_common::FilePathMapping::empty(), )); @@ -1150,13 +1160,17 @@ impl Graph { .bundle(entries) .context("Unable to output bundle during Graph::bundle().")?; let mut buf = Vec::new(); + let mut src_map_buf = Vec::new(); { let mut emitter = swc_ecmascript::codegen::Emitter { cfg: swc_ecmascript::codegen::Config { minify: false }, cm: cm.clone(), comments: None, wr: Box::new(swc_ecmascript::codegen::text_writer::JsWriter::new( - cm, "\n", &mut buf, None, + cm.clone(), + "\n", + &mut buf, + Some(&mut src_map_buf), )), }; @@ -1164,8 +1178,24 @@ impl Graph { .emit_module(&output[0].module) .context("Unable to emit bundle during Graph::bundle().")?; } + let mut src = String::from_utf8(buf) + .context("Emitted bundle is an invalid utf-8 string.")?; + let mut map: Option = None; + { + let mut buf = Vec::new(); + cm.build_source_map_from(&mut src_map_buf, None) + .to_writer(&mut buf)?; - String::from_utf8(buf).context("Emitted bundle is an invalid utf-8 string.") + if emit_options.inline_source_map { + src.push_str("//# sourceMappingURL=data:application/json;base64,"); + let encoded_map = base64::encode(buf); + src.push_str(&encoded_map); + } else if emit_options.source_map { + map = Some(String::from_utf8(buf)?); + } + } + + Ok((src, map)) } /// Update the handler with any modules that are marked as _dirty_ and update @@ -1606,6 +1636,7 @@ impl Graph { "emitDecoratorMetadata": false, "importsNotUsedAsValues": "remove", "inlineSourceMap": true, + "sourceMap": false, "jsx": "react", "jsxFactory": "React.createElement", "jsxFragmentFactory": "React.Fragment", @@ -2413,9 +2444,10 @@ pub mod tests { .expect("should have emitted"); assert!(result_info.diagnostics.is_empty()); assert!(result_info.maybe_ignored_options.is_none()); - assert_eq!(emitted_files.len(), 1); + assert_eq!(emitted_files.len(), 2); let actual = emitted_files.get("deno:///bundle.js"); assert!(actual.is_some()); + assert!(emitted_files.contains_key("deno:///bundle.js.map")); let actual = actual.unwrap(); assert!(actual.contains("const b = \"b\";")); assert!(actual.contains("console.log(mod);")); diff --git a/cli/tests/compiler_api_test.ts b/cli/tests/compiler_api_test.ts index c6e7de6515..00116e7e1d 100644 --- a/cli/tests/compiler_api_test.ts +++ b/cli/tests/compiler_api_test.ts @@ -2,6 +2,7 @@ import { assert, assertEquals, + assertStringIncludes, assertThrowsAsync, } from "../../test_util/std/testing/asserts.ts"; @@ -188,7 +189,10 @@ Deno.test({ assertEquals(diagnostics.length, 0); assert(!ignoredOptions); assertEquals(stats.length, 12); - assertEquals(Object.keys(files), ["deno:///bundle.js"]); + assertEquals( + Object.keys(files).sort(), + ["deno:///bundle.js", "deno:///bundle.js.map"].sort(), + ); assert(files["deno:///bundle.js"].includes(`const bar1 = "bar"`)); }, }); @@ -205,7 +209,10 @@ Deno.test({ assertEquals(diagnostics.length, 0); assert(!ignoredOptions); assertEquals(stats.length, 12); - assertEquals(Object.keys(files), ["deno:///bundle.js"]); + assertEquals( + Object.keys(files).sort(), + ["deno:///bundle.js", "deno:///bundle.js.map"].sort(), + ); assert(files["deno:///bundle.js"].length); }, }); @@ -226,7 +233,10 @@ Deno.test({ assertEquals(diagnostics.length, 0); assert(!ignoredOptions); assertEquals(stats.length, 12); - assertEquals(Object.keys(files), ["deno:///bundle.js"]); + assertEquals( + Object.keys(files).sort(), + ["deno:///bundle.js.map", "deno:///bundle.js"].sort(), + ); assert(files["deno:///bundle.js"].includes(`const bar1 = "bar"`)); }, }); @@ -333,9 +343,10 @@ Deno.test({ }); assert(diagnostics); assertEquals(diagnostics.length, 0); - assertEquals(Object.keys(files).length, 1); + assertEquals(Object.keys(files).length, 2); assert(files["deno:///bundle.js"].startsWith("(function() {\n")); assert(files["deno:///bundle.js"].endsWith("})();\n")); + assert(files["deno:///bundle.js.map"]); }, }); @@ -357,3 +368,41 @@ Deno.test({ ); }, }); + +Deno.test({ + name: `Deno.emit() - support source maps with bundle option`, + async fn() { + { + const { diagnostics, files } = await Deno.emit("/a.ts", { + bundle: "classic", + sources: { + "/a.ts": `import { b } from "./b.ts"; + console.log(b);`, + "/b.ts": `export const b = "b";`, + }, + compilerOptions: { + inlineSourceMap: true, + sourceMap: false, + }, + }); + assert(diagnostics); + assertEquals(diagnostics.length, 0); + assertEquals(Object.keys(files).length, 1); + assertStringIncludes(files["deno:///bundle.js"], "sourceMappingURL"); + } + + const { diagnostics, files } = await Deno.emit("/a.ts", { + bundle: "classic", + sources: { + "/a.ts": `import { b } from "./b.ts"; + console.log(b);`, + "/b.ts": `export const b = "b";`, + }, + }); + assert(diagnostics); + assertEquals(diagnostics.length, 0); + assertEquals(Object.keys(files).length, 2); + assert(files["deno:///bundle.js"]); + assert(files["deno:///bundle.js.map"]); + }, +});