diff --git a/Cargo.lock b/Cargo.lock index 3a8532aa31d0f..162735ec70677 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9111,6 +9111,8 @@ dependencies = [ "serde", "serde_json", "serde_qs", + "sourcemap", + "swc_core", "turbo-tasks", "turbo-tasks-build", "turbo-tasks-fs", @@ -9206,6 +9208,7 @@ name = "turbopack-core" version = "0.1.0" dependencies = [ "anyhow", + "async-recursion", "async-trait", "auto-hash-map", "browserslist-rs", diff --git a/Cargo.toml b/Cargo.toml index 6ca635fbbc370..70c29d6d441c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ lto = "off" # Declare dependencies used across workspace packages requires single version bump. # ref: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace [workspace.dependencies] +async-recursion = "1.0.2" # Keep consistent with preset_env_base through swc_core browserslist-rs = { version = "0.12.2" } mdxjs = { version = "0.1.14" } @@ -213,6 +214,7 @@ serde_qs = "0.11.0" serde_with = "2.3.2" serde_yaml = "0.9.17" sha2 = "0.10.6" +sourcemap = "6.0.2" syn = "1.0.107" tempfile = "3.3.0" test-case = "3.0.0" diff --git a/crates/turbopack-build/Cargo.toml b/crates/turbopack-build/Cargo.toml index 21b971f575ef2..2eb8773d665b3 100644 --- a/crates/turbopack-build/Cargo.toml +++ b/crates/turbopack-build/Cargo.toml @@ -22,6 +22,8 @@ indoc = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_qs = { workspace = true } +sourcemap = "6.0.2" +swc_core = { workspace = true, features = ["__parser", "ecma_minifier"] } turbo-tasks = { workspace = true } turbo-tasks-fs = { workspace = true } diff --git a/crates/turbopack-build/src/chunking_context.rs b/crates/turbopack-build/src/chunking_context.rs index ca18e1e1fd5b0..59bdb40792aa5 100644 --- a/crates/turbopack-build/src/chunking_context.rs +++ b/crates/turbopack-build/src/chunking_context.rs @@ -22,12 +22,23 @@ use crate::ecmascript::node::{ chunk::EcmascriptBuildNodeChunk, entry::chunk::EcmascriptBuildNodeEntryChunk, }; +#[turbo_tasks::value(shared)] +pub enum MinifyType { + Minify, + NoMinify, +} + /// A builder for [`Vc`]. pub struct BuildChunkingContextBuilder { context: BuildChunkingContext, } impl BuildChunkingContextBuilder { + pub fn minify_type(mut self, minify_type: Vc) -> Self { + self.context.minify_type = minify_type; + self + } + pub fn runtime_type(mut self, runtime_type: RuntimeType) -> Self { self.context.runtime_type = runtime_type; self @@ -63,6 +74,8 @@ pub struct BuildChunkingContext { environment: Vc, /// The kind of runtime to include in the output. runtime_type: RuntimeType, + /// Whether to minify resulting chunks + minify_type: Vc, } impl BuildChunkingContext { @@ -83,6 +96,7 @@ impl BuildChunkingContext { layer: None, environment, runtime_type: Default::default(), + minify_type: MinifyType::Minify.cell(), }, } } @@ -96,6 +110,10 @@ impl BuildChunkingContext { pub fn runtime_type(&self) -> RuntimeType { self.runtime_type } + + pub fn minify_type(&self) -> Vc { + self.minify_type + } } #[turbo_tasks::value_impl] diff --git a/crates/turbopack-build/src/ecmascript/node/content.rs b/crates/turbopack-build/src/ecmascript/node/content.rs index 73f6276069444..dcabd0cd6eed0 100644 --- a/crates/turbopack-build/src/ecmascript/node/content.rs +++ b/crates/turbopack-build/src/ecmascript/node/content.rs @@ -16,7 +16,7 @@ use turbopack_ecmascript::{ }; use super::chunk::EcmascriptBuildNodeChunk; -use crate::BuildChunkingContext; +use crate::{chunking_context::MinifyType, minify::minify, BuildChunkingContext}; #[turbo_tasks::value] pub(super) struct EcmascriptBuildNodeChunkContent { @@ -47,7 +47,8 @@ impl EcmascriptBuildNodeChunkContent { #[turbo_tasks::function] async fn code(self: Vc) -> Result> { let this = self.await?; - let chunk_path = this.chunk.ident().path().await?; + let chunk_path_vc = this.chunk.ident().path(); + let chunk_path = chunk_path_vc.await?; let mut code = CodeBuilder::default(); @@ -85,8 +86,15 @@ impl EcmascriptBuildNodeChunkContent { write!(code, "\n\n//# sourceMappingURL={}.map", filename)?; } - let code = code.build(); - Ok(code.cell()) + let code = code.build().cell(); + if matches!( + &*this.chunking_context.await?.minify_type().await?, + MinifyType::Minify + ) { + return Ok(minify(chunk_path_vc, code)); + } + + Ok(code) } #[turbo_tasks::function] diff --git a/crates/turbopack-build/src/lib.rs b/crates/turbopack-build/src/lib.rs index 0226b408aedb4..1a10c3def3262 100644 --- a/crates/turbopack-build/src/lib.rs +++ b/crates/turbopack-build/src/lib.rs @@ -5,8 +5,9 @@ pub(crate) mod chunking_context; pub(crate) mod ecmascript; +pub(crate) mod minify; -pub use chunking_context::{BuildChunkingContext, BuildChunkingContextBuilder}; +pub use chunking_context::{BuildChunkingContext, BuildChunkingContextBuilder, MinifyType}; pub fn register() { turbo_tasks::register(); diff --git a/crates/turbopack-build/src/minify.rs b/crates/turbopack-build/src/minify.rs new file mode 100644 index 0000000000000..687cc97202f8f --- /dev/null +++ b/crates/turbopack-build/src/minify.rs @@ -0,0 +1,130 @@ +use std::{io::Write, sync::Arc}; + +use anyhow::{Context, Result}; +use swc_core::{ + base::{try_with_handler, Compiler}, + common::{ + BytePos, FileName, FilePathMapping, LineCol, Mark, SourceMap as SwcSourceMap, GLOBALS, + }, + ecma::{self, ast::Program, codegen::Node}, +}; +use turbo_tasks::Vc; +use turbo_tasks_fs::FileSystemPath; +use turbopack_core::{ + code_builder::{Code, CodeBuilder}, + source_map::GenerateSourceMap, +}; +use turbopack_ecmascript::ParseResultSourceMap; + +#[turbo_tasks::function] +pub async fn minify(path: Vc, code: Vc) -> Result> { + let original_map = *code.generate_source_map().await?; + let minified_code = perform_minify(path, code); + + let merged = match (original_map, *minified_code.generate_source_map().await?) { + (Some(original_map), Some(minify_map)) => Some(Vc::upcast(original_map.trace(minify_map))), + _ => None, + }; + + let mut builder = CodeBuilder::default(); + builder.push_source(minified_code.await?.source_code(), merged); + let path = &*path.await?; + let filename = path.file_name(); + write!(builder, "\n\n//# sourceMappingURL={}.map", filename)?; + Ok(builder.build().cell()) +} + +#[turbo_tasks::function] +async fn perform_minify(path: Vc, code_vc: Vc) -> Result> { + let code = &*code_vc.await?; + let cm = Arc::new(SwcSourceMap::new(FilePathMapping::empty())); + let compiler = Arc::new(Compiler::new(cm.clone())); + let fm = compiler.cm.new_source_file( + FileName::Custom((*path.await?.path).to_string()), + code.source_code().to_str()?.to_string(), + ); + + let lexer = ecma::parser::lexer::Lexer::new( + ecma::parser::Syntax::default(), + ecma::ast::EsVersion::latest(), + ecma::parser::StringInput::from(&*fm), + None, + ); + let mut parser = ecma::parser::Parser::new_from(lexer); + let program = try_with_handler(cm.clone(), Default::default(), |handler| { + GLOBALS.set(&Default::default(), || { + let program = parser.parse_program().unwrap(); + let unresolved_mark = Mark::new(); + let top_level_mark = Mark::new(); + + Ok(compiler.run_transform(handler, false, || { + swc_core::ecma::minifier::optimize( + program, + cm.clone(), + None, + None, + &ecma::minifier::option::MinifyOptions { + compress: Some(Default::default()), + ..Default::default() + }, + &ecma::minifier::option::ExtraOptions { + top_level_mark, + unresolved_mark, + }, + ) + })) + }) + })?; + + let (src, src_map_buf) = print_program(cm.clone(), program)?; + + let mut builder = CodeBuilder::default(); + builder.push_source( + &src.into(), + Some(*Box::new(Vc::upcast( + ParseResultSourceMap::new(cm, src_map_buf).cell(), + ))), + ); + + Ok(builder.build().cell()) +} + +// From https://github.com/swc-project/swc/blob/11efd4e7c5e8081f8af141099d3459c3534c1e1d/crates/swc/src/lib.rs#L523-L560 +fn print_program( + cm: Arc, + program: Program, +) -> Result<(String, Vec<(BytePos, LineCol)>)> { + let mut src_map_buf = vec![]; + + let src = { + let mut buf = vec![]; + { + let wr = Box::new(swc_core::ecma::codegen::text_writer::omit_trailing_semi( + Box::new(swc_core::ecma::codegen::text_writer::JsWriter::new( + cm.clone(), + "\n", + &mut buf, + Some(&mut src_map_buf), + )), + )) as Box; + + let mut emitter = swc_core::ecma::codegen::Emitter { + cfg: swc_core::ecma::codegen::Config { + minify: true, + ..Default::default() + }, + comments: None, + cm: cm.clone(), + wr, + }; + + program + .emit_with(&mut emitter) + .context("failed to emit module")?; + } + // Invalid utf8 is valid in javascript world. + String::from_utf8(buf).expect("invalid utf8 character detected") + }; + + Ok((src, src_map_buf)) +} diff --git a/crates/turbopack-cli/src/arguments.rs b/crates/turbopack-cli/src/arguments.rs index d1767eb50ec4e..e76fd4243911f 100644 --- a/crates/turbopack-cli/src/arguments.rs +++ b/crates/turbopack-cli/src/arguments.rs @@ -103,4 +103,8 @@ pub struct DevArguments { pub struct BuildArguments { #[clap(flatten)] pub common: CommonArguments, + + /// Don't minify build output. + #[clap(long)] + pub no_minify: bool, } diff --git a/crates/turbopack-cli/src/build/mod.rs b/crates/turbopack-cli/src/build/mod.rs index 6f02bd496fc66..01791bb9bec53 100644 --- a/crates/turbopack-cli/src/build/mod.rs +++ b/crates/turbopack-cli/src/build/mod.rs @@ -10,7 +10,7 @@ use turbo_tasks::{unit, TransientInstance, TryJoinIterExt, TurboTasks, Value, Vc use turbo_tasks_fs::FileSystem; use turbo_tasks_memory::MemoryBackend; use turbopack::ecmascript::EcmascriptModuleAsset; -use turbopack_build::BuildChunkingContext; +use turbopack_build::{BuildChunkingContext, MinifyType}; use turbopack_cli_utils::issue::{ConsoleUi, LogOptions}; use turbopack_core::{ asset::Asset, @@ -53,6 +53,7 @@ pub struct TurbopackBuildBuilder { log_level: IssueSeverity, show_all: bool, log_detail: bool, + minify_type: MinifyType, } impl TurbopackBuildBuilder { @@ -70,6 +71,7 @@ impl TurbopackBuildBuilder { log_level: IssueSeverity::Warning, show_all: false, log_detail: false, + minify_type: MinifyType::Minify, } } @@ -98,6 +100,11 @@ impl TurbopackBuildBuilder { self } + pub fn minify_type(mut self, minify_type: MinifyType) -> Self { + self.minify_type = minify_type; + self + } + pub async fn build(self) -> Result<()> { let task = self.turbo_tasks.spawn_once_task(async move { let build_result = build_internal( @@ -112,6 +119,7 @@ impl TurbopackBuildBuilder { ) .cell(), self.browserslist_query, + self.minify_type.cell(), ); // Await the result to propagate any errors. @@ -150,6 +158,7 @@ async fn build_internal( root_dir: String, entry_requests: Vc, browserslist_query: String, + minify_type: Vc, ) -> Result> { let env = Environment::new(Value::new(ExecutionEnvironment::Browser( BrowserEnvironment { @@ -178,6 +187,7 @@ async fn build_internal( build_output_root, env, ) + .minify_type(minify_type) .build(), ); @@ -298,12 +308,17 @@ pub async fn build(args: &BuildArguments) -> Result<()> { let mut builder = TurbopackBuildBuilder::new(tt, project_dir, root_dir) .log_detail(args.common.log_detail) - .show_all(args.common.show_all) .log_level( args.common .log_level .map_or_else(|| IssueSeverity::Warning, |l| l.0), - ); + ) + .minify_type(if args.no_minify { + MinifyType::NoMinify + } else { + MinifyType::Minify + }) + .show_all(args.common.show_all); for entry in normalize_entries(&args.common.entries) { builder = builder.entry_request(EntryRequest::Relative(entry)); diff --git a/crates/turbopack-core/Cargo.toml b/crates/turbopack-core/Cargo.toml index 1aa251170465d..f09aa345a45da 100644 --- a/crates/turbopack-core/Cargo.toml +++ b/crates/turbopack-core/Cargo.toml @@ -11,6 +11,7 @@ bench = false [dependencies] anyhow = { workspace = true } +async-recursion = { workspace = true } async-trait = { workspace = true } auto-hash-map = { workspace = true } browserslist-rs = { workspace = true } diff --git a/crates/turbopack-core/src/source_map/mod.rs b/crates/turbopack-core/src/source_map/mod.rs index 62e0d11357315..ec32309daf8d5 100644 --- a/crates/turbopack-core/src/source_map/mod.rs +++ b/crates/turbopack-core/src/source_map/mod.rs @@ -13,6 +13,12 @@ pub(crate) mod source_map_asset; pub use source_map_asset::SourceMapAsset; +// #[turbo_tasks::value] +// struct CrateTokens(CrateRawToken); + +/// Represents an empty value in a u32 variable in the source_map crate. +static SOURCEMAP_CRATE_NONE_U32: u32 = !0; + /// Allows callers to generate source maps. #[turbo_tasks::value_trait] pub trait GenerateSourceMap { @@ -43,11 +49,16 @@ pub struct SectionMapping(IndexMap>>); #[turbo_tasks::value(transparent)] pub struct OptionSourceMap(Option>); +#[turbo_tasks::value(transparent)] +#[derive(Clone)] +pub struct Tokens(Vec); + /// A token represents a mapping in a source map. It may either be Synthetic, /// meaning it was generated by some build tool and doesn't represent a location /// in a user-authored source file, or it is Original, meaning it represents a /// real location in source file. #[turbo_tasks::value] +#[derive(Clone)] pub enum Token { Synthetic(SyntheticToken), Original(OriginalToken), @@ -56,6 +67,7 @@ pub enum Token { /// A SyntheticToken represents a region of the generated file that was created /// by some build tool. #[turbo_tasks::value] +#[derive(Clone)] pub struct SyntheticToken { generated_line: usize, generated_column: usize, @@ -64,6 +76,7 @@ pub struct SyntheticToken { /// An OriginalToken represents a region of the generated file that exists in /// user-authored source file. #[turbo_tasks::value] +#[derive(Clone)] pub struct OriginalToken { pub generated_line: usize, pub generated_column: usize, @@ -73,6 +86,22 @@ pub struct OriginalToken { pub name: Option, } +impl Token { + pub fn generated_line(&self) -> usize { + match self { + Self::Original(t) => t.generated_line, + Self::Synthetic(t) => t.generated_line, + } + } + + pub fn generated_column(&self) -> usize { + match self { + Self::Original(t) => t.generated_column, + Self::Synthetic(t) => t.generated_column, + } + } +} + #[turbo_tasks::value(transparent)] pub struct OptionToken(Option); @@ -99,6 +128,34 @@ impl<'a> From> for Token { } } +impl TryInto for Token { + type Error = std::num::ParseIntError; + + fn try_into(self) -> Result { + Ok(match self { + Self::Original(t) => sourcemap::RawToken { + dst_col: t.generated_column as u32, + dst_line: t.generated_line as u32, + name_id: match t.name { + None => SOURCEMAP_CRATE_NONE_U32, + Some(name) => name.parse()?, + }, + src_col: t.original_column as u32, + src_line: t.original_line as u32, + src_id: t.original_file.parse()?, + }, + Self::Synthetic(t) => sourcemap::RawToken { + dst_col: t.generated_column as u32, + dst_line: t.generated_line as u32, + name_id: SOURCEMAP_CRATE_NONE_U32, + src_col: SOURCEMAP_CRATE_NONE_U32, + src_line: SOURCEMAP_CRATE_NONE_U32, + src_id: SOURCEMAP_CRATE_NONE_U32, + }, + }) + } +} + impl SourceMap { /// Creates a new SourceMap::Regular Vc out of a sourcemap::SourceMap /// ("CrateMap") instance. @@ -190,6 +247,19 @@ impl SourceMap { Ok(rope.cell()) } + #[turbo_tasks::function] + pub async fn tokens(self: Vc) -> Result> { + let this = &*self.await?; + Ok(Tokens(match this { + Self::Regular(m) => (&****m).tokens().map(|t| t.into()).collect(), + Self::Sectioned(m) => (&***m.flatten().await?) + .tokens() + .map(|t| t.into()) + .collect(), + }) + .cell()) + } + /// Traces a generated line/column into an mapping token representing either /// synthetic code or user-authored original code. #[turbo_tasks::function] @@ -199,12 +269,13 @@ impl SourceMap { column: usize, ) -> Result> { let token = match &*self.await? { - SourceMap::Regular(map) => map - .lookup_token(line as u32, column as u32) - // The sourcemap crate incorrectly returns a previous line's token when there's - // not a match on this line. - .filter(|t| t.get_dst_line() == line as u32) - .map(Token::from), + SourceMap::Regular(map) => { + map.lookup_token(line as u32, column as u32) + // The sourcemap crate incorrectly returns a previous line's token when there's + // not a match on this line. + .filter(|t| t.get_dst_line() == line as u32) + .map(Token::from) + } SourceMap::Sectioned(map) => { let len = map.sections.len(); @@ -245,10 +316,76 @@ impl SourceMap { }; Ok(OptionToken(token).cell()) } + + #[turbo_tasks::function] + pub async fn trace(self: Vc, other: Vc) -> Result> { + let own_map = match &*self.await? { + Self::Regular(m) => m.clone(), + Self::Sectioned(m) => m.flatten().await?, + }; + + let mut builder = sourcemap::SourceMapBuilder::new(own_map.get_file()); + let other_tokens = other.tokens().await?; + let tokens = other_tokens.iter().map(|other_token| { + let other_token = match other_token { + Token::Synthetic(_) => panic!("Unexpected synthetic token"), + Token::Original(t) => t, + }; + + ( + own_map.lookup_token( + other_token.original_line as u32, + other_token.original_column as u32, + ), + other_token, + ) + }); + + let mut source_to_src_id = IndexMap::new(); + for (traced_token, other_token) in tokens { + if let Some(traced_token) = traced_token { + let original_file = traced_token.get_source(); + let token = builder.add( + other_token.generated_line as u32, + other_token.generated_column as u32, + traced_token.get_src_line(), + traced_token.get_src_col(), + original_file, + traced_token.get_name(), + ); + + if let Some(original_file) = original_file { + source_to_src_id.insert(original_file, token.src_id); + } + } + } + + for (src_id, source) in own_map.sources().enumerate() { + if let Some(our_src_id) = source_to_src_id.get(source) { + builder + .set_source_contents(*our_src_id, own_map.get_source_contents(src_id as u32)); + } + } + + Ok(Self::new_regular(builder.into_sourcemap()).into()) + } +} + +#[turbo_tasks::value_impl] +impl GenerateSourceMap for SourceMap { + #[turbo_tasks::function] + fn generate_source_map(self: Vc) -> Vc { + Vc::cell(Some(self)) + } + + #[turbo_tasks::function] + fn by_section(&self, _section: String) -> Vc { + Vc::cell(None) + } } /// A regular source map covers an entire file. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct RegularSourceMap(Arc); impl RegularSourceMap { @@ -320,6 +457,47 @@ impl SectionedSourceMap { pub fn new(sections: Vec) -> Self { Self { sections } } + + #[async_recursion::async_recursion] + pub async fn flatten(&self) -> Result { + // Derived from https://github.com/getsentry/rust-sourcemap/blob/f1b758a251a5edddc0767f58f8391912c062c889/src/types.rs#L946 + + let mut builder = SourceMapBuilder::new(None); + for section in &self.sections { + let map = match &*section.map.await? { + SourceMap::Regular(m) => (***m).clone(), + SourceMap::Sectioned(m) => (***m.flatten().await?).clone(), + }; + + for token in map.tokens() { + let dst_line = token.get_dst_line(); + let raw = builder.add( + dst_line + section.offset.line as u32, + token.get_dst_col() + + if dst_line == 0 { + section.offset.column as u32 + } else { + 0 + }, + token.get_src_line(), + token.get_src_col(), + token.get_source(), + token.get_name(), + ); + + if token.get_source().is_some() && !builder.has_source_contents(raw.src_id) { + builder.set_source_contents( + raw.src_id, + map.get_source_contents(token.get_src_id()), + ); + } + } + } + + Ok(RegularSourceMap(Arc::new(CrateMapWrapper( + builder.into_sourcemap(), + )))) + } } /// A section of a larger sectioned source map, which applies at source diff --git a/crates/turbopack-core/src/source_map/source_map.rs b/crates/turbopack-core/src/source_map/source_map.rs deleted file mode 100644 index e9e94514472aa..0000000000000 --- a/crates/turbopack-core/src/source_map/source_map.rs +++ /dev/null @@ -1,333 +0,0 @@ -use std::{io::Write, ops::Deref, sync::Arc}; - -use anyhow::Result; -use indexmap::IndexMap; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use sourcemap::{SourceMap as CrateMap, SourceMapBuilder}; -use turbo_tasks::{TryJoinIterExt, Vc}; -use turbo_tasks_fs::rope::{Rope, RopeBuilder}; - -use crate::source_pos::SourcePos; - -/// Allows callers to generate source maps. -#[turbo_tasks::value_trait] -pub trait GenerateSourceMap { - /// Generates a usable source map, capable of both tracing and stringifying. - fn generate_source_map(self: Vc) -> Vc; - - /// Returns an individual section of the larger source map, if found. - fn by_section(&self, _section: String) -> Vc { - Vc::cell(None) - } -} - -/// The source map spec lists 2 formats, a regular format where a single map -/// covers the entire file, and an "index" sectioned format where multiple maps -/// cover different regions of the file. -#[turbo_tasks::value(shared)] -pub enum SourceMap { - /// A regular source map covers an entire file. - Regular(#[turbo_tasks(trace_ignore)] RegularSourceMap), - /// A sectioned source map contains many (possibly recursive) maps covering - /// different regions of the file. - Sectioned(#[turbo_tasks(trace_ignore)] SectionedSourceMap), -} - -#[turbo_tasks::value(transparent)] -pub struct SectionMapping(IndexMap>>); - -#[turbo_tasks::value(transparent)] -pub struct OptionSourceMap(Option>); - -/// A token represents a mapping in a source map. It may either be Synthetic, -/// meaning it was generated by some build tool and doesn't represent a location -/// in a user-authored source file, or it is Original, meaning it represents a -/// real location in source file. -#[turbo_tasks::value] -pub enum Token { - Synthetic(SyntheticToken), - Original(OriginalToken), -} - -/// A SyntheticToken represents a region of the generated file that was created -/// by some build tool. -#[turbo_tasks::value] -pub struct SyntheticToken { - generated_line: usize, - generated_column: usize, -} - -/// An OriginalToken represents a region of the generated file that exists in -/// user-authored source file. -#[turbo_tasks::value] -pub struct OriginalToken { - pub generated_line: usize, - pub generated_column: usize, - pub original_file: String, - pub original_line: usize, - pub original_column: usize, - pub name: Option, -} - -#[turbo_tasks::value(transparent)] -pub struct OptionToken(Option); - -impl<'a> From> for Token { - fn from(t: sourcemap::Token) -> Self { - if t.has_source() { - Token::Original(OriginalToken { - generated_line: t.get_dst_line() as usize, - generated_column: t.get_dst_col() as usize, - original_file: t - .get_source() - .expect("already checked token has source") - .to_string(), - original_line: t.get_src_line() as usize, - original_column: t.get_src_col() as usize, - name: t.get_name().map(String::from), - }) - } else { - Token::Synthetic(SyntheticToken { - generated_line: t.get_dst_line() as usize, - generated_column: t.get_dst_col() as usize, - }) - } - } -} - -impl SourceMap { - /// Creates a new SourceMap::Regular Vc out of a sourcemap::SourceMap - /// ("CrateMap") instance. - pub fn new_regular(map: CrateMap) -> Self { - SourceMap::Regular(RegularSourceMap::new(map)) - } - - /// Creates a new SourceMap::Sectioned Vc out of a collection of source map - /// sections. - pub fn new_sectioned(sections: Vec) -> Self { - SourceMap::Sectioned(SectionedSourceMap::new(sections)) - } -} - -#[turbo_tasks::value_impl] -impl SourceMap { - /// A source map that contains no actual source location information (no - /// `sources`, no mappings that point into a source). This is used to tell - /// Chrome that the generated code starting at a particular offset is no - /// longer part of the previous section's mappings. - #[turbo_tasks::function] - pub fn empty() -> Vc { - let mut builder = SourceMapBuilder::new(None); - builder.add(0, 0, 0, 0, None, None); - SourceMap::new_regular(builder.into_sourcemap()).cell() - } -} - -#[turbo_tasks::value_impl] -impl SourceMap { - /// Stringifies the source map into JSON bytes. - #[turbo_tasks::function] - pub async fn to_rope(self: Vc) -> Result> { - let this = self.await?; - let rope = match &*this { - SourceMap::Regular(r) => { - let mut bytes = vec![]; - r.0.to_writer(&mut bytes)?; - Rope::from(bytes) - } - - SourceMap::Sectioned(s) => { - if s.sections.len() == 1 { - let s = &s.sections[0]; - if s.offset == (0, 0) { - return Ok(s.map.to_rope()); - } - } - - // My kingdom for a decent dedent macro with interpolation! - let mut rope = RopeBuilder::from( - r#"{ - "version": 3, - "sections": ["#, - ); - - let sections = s - .sections - .iter() - .map(|s| async move { Ok((s.offset, s.map.to_rope().await?)) }) - .try_join() - .await?; - - let mut first_section = true; - for (offset, section_map) in sections { - if !first_section { - rope += ","; - } - first_section = false; - - write!( - rope, - r#" - {{"offset": {{"line": {}, "column": {}}}, "map": "#, - offset.line, offset.column, - )?; - - rope += &*section_map; - - rope += "}"; - } - - rope += "] -}"; - - rope.build() - } - }; - Ok(rope.cell()) - } - - /// Traces a generated line/column into an mapping token representing either - /// synthetic code or user-authored original code. - #[turbo_tasks::function] - pub async fn lookup_token( - self: Vc, - line: usize, - column: usize, - ) -> Result> { - let token = match &*self.await? { - SourceMap::Regular(map) => map - .lookup_token(line as u32, column as u32) - // The sourcemap crate incorrectly returns a previous line's token when there's - // not a match on this line. - .filter(|t| t.get_dst_line() == line as u32) - .map(Token::from), - - SourceMap::Sectioned(map) => { - let len = map.sections.len(); - let mut low = 0; - let mut high = len; - let pos = SourcePos { line, column }; - - // A "greatest lower bound" binary search. We're looking for the closest section - // offset <= to our line/col. - while low < high { - let mid = (low + high) / 2; - if pos < map.sections[mid].offset { - high = mid; - } else { - low = mid + 1; - } - } - - // Our GLB search will return the section immediately to the right of the - // section we actually want to recurse into, because the binary search does not - // early exit on an exact match (it'll `low = mid + 1`). - if low > 0 && low <= len { - let SourceMapSection { map, offset } = &map.sections[low - 1]; - // We're looking for the position `l` lines into region covered by this - // sourcemap's section. - let l = line - offset.line; - // The source map starts offset by the section's column only on its first line. - // On the 2nd+ line, the source map covers starting at column 0. - let c = if line == offset.line { - column - offset.column - } else { - column - }; - return Ok(map.lookup_token(l, c)); - } - None - } - }; - Ok(OptionToken(token).cell()) - } -} - -/// A regular source map covers an entire file. -#[derive(Debug, Serialize, Deserialize)] -pub struct RegularSourceMap(Arc); - -impl RegularSourceMap { - fn new(map: CrateMap) -> Self { - RegularSourceMap(Arc::new(CrateMapWrapper(map))) - } -} - -impl Deref for RegularSourceMap { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Eq for RegularSourceMap {} -impl PartialEq for RegularSourceMap { - fn eq(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.0, &other.0) - } -} - -/// Wraps the CrateMap struct so that it can be cached in a Vc. -#[derive(Debug)] -pub struct CrateMapWrapper(sourcemap::SourceMap); - -// Safety: CrateMap contains a raw pointer, which isn't Send, which is required -// to cache in a Vc. So, we have wrap it in 4 layers of cruft to do it. We don't -// actually use the pointer, because we don't perform sourcesContent lookups, -// so it's fine. -unsafe impl Send for CrateMapWrapper {} -unsafe impl Sync for CrateMapWrapper {} - -impl Deref for CrateMapWrapper { - type Target = sourcemap::SourceMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Serialize for CrateMapWrapper { - fn serialize(&self, serializer: S) -> Result { - use serde::ser::Error; - let mut bytes = vec![]; - self.0.to_writer(&mut bytes).map_err(Error::custom)?; - serializer.serialize_bytes(bytes.as_slice()) - } -} - -impl<'de> Deserialize<'de> for CrateMapWrapper { - fn deserialize>(deserializer: D) -> Result { - use serde::de::Error; - let bytes = <&[u8]>::deserialize(deserializer)?; - let map = CrateMap::from_slice(bytes).map_err(Error::custom)?; - Ok(CrateMapWrapper(map)) - } -} - -/// A sectioned source map contains many (possibly recursive) maps covering -/// different regions of the file. -#[derive(Eq, PartialEq, Debug, Serialize, Deserialize)] -pub struct SectionedSourceMap { - sections: Vec, -} - -impl SectionedSourceMap { - pub fn new(sections: Vec) -> Self { - Self { sections } - } -} - -/// A section of a larger sectioned source map, which applies at source -/// positions >= the offset (until the next section starts). -#[derive(Eq, PartialEq, Debug, Serialize, Deserialize)] -pub struct SourceMapSection { - offset: SourcePos, - map: Vc, -} - -impl SourceMapSection { - pub fn new(offset: SourcePos, map: Vc) -> Self { - Self { offset, map } - } -} diff --git a/crates/turbopack/Cargo.toml b/crates/turbopack/Cargo.toml index 0e84f404ba522..c1dd1d77782c4 100644 --- a/crates/turbopack/Cargo.toml +++ b/crates/turbopack/Cargo.toml @@ -15,7 +15,7 @@ bench_against_node_nft = [] [dependencies] anyhow = { workspace = true } -async-recursion = "1.0.2" +async-recursion = { workspace = true } futures = { workspace = true } indexmap = { workspace = true, features = ["serde"] } lazy_static = { workspace = true }