diff --git a/Cargo.lock b/Cargo.lock index 065fc8581b9581..ef04acbd99e80f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7524,6 +7524,7 @@ dependencies = [ "rstest", "serde", "serde_json", + "serde_path_to_error", "sha2", "tempfile", "tokio", diff --git a/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index cbac266de6c294..addaf47432a5a4 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -8,6 +8,7 @@ use turbo_tasks::{ Value, }; use turbo_tasks_env::EnvMapVc; +use turbo_tasks_fs::json::parse_json_rope_with_source_context; use turbopack::{ evaluate_context::node_evaluate_asset_context, module_options::{WebpackLoadersOptions, WebpackLoadersOptionsVc}, @@ -485,7 +486,7 @@ pub async fn load_next_config(execution_context: ExecutionContextVc) -> Result { - let next_config: NextConfig = serde_json::from_reader(val.read())?; + let next_config: NextConfig = parse_json_rope_with_source_context(val)?; let next_config = next_config.cell(); Ok(next_config) diff --git a/crates/next-core/src/next_font_google/mod.rs b/crates/next-core/src/next_font_google/mod.rs index 6aef378d4923f0..a35ecfa9a9def8 100644 --- a/crates/next-core/src/next_font_google/mod.rs +++ b/crates/next-core/src/next_font_google/mod.rs @@ -4,7 +4,7 @@ use indoc::formatdoc; use once_cell::sync::Lazy; use turbo_tasks::primitives::{OptionStringVc, OptionU16Vc, StringVc, U32Vc}; use turbo_tasks_fetch::fetch; -use turbo_tasks_fs::{FileContent, FileSystemPathVc}; +use turbo_tasks_fs::{json::parse_json_with_source_context, FileContent, FileSystemPathVc}; use turbo_tasks_hash::hash_xxh3_hash64; use turbopack_core::{ issue::IssueSeverity, @@ -34,8 +34,9 @@ pub(crate) mod request; mod util; pub const GOOGLE_FONTS_STYLESHEET_URL: &str = "https://fonts.googleapis.com/css2"; -static FONT_DATA: Lazy = - Lazy::new(|| serde_json::from_str(include_str!("__generated__/font-data.json")).unwrap()); +static FONT_DATA: Lazy = Lazy::new(|| { + parse_json_with_source_context(include_str!("__generated__/font-data.json")).unwrap() +}); type FontData = IndexMap; @@ -371,6 +372,6 @@ async fn font_options_from_query_map(query: QueryMapVc) -> Result Result<()> { - let data: IndexMap = serde_json::from_str( + let data: IndexMap = parse_json_with_source_context( r#" { "ABeeZee": { @@ -196,7 +197,7 @@ mod tests { "#, )?; - let request: NextFontRequest = serde_json::from_str( + let request: NextFontRequest = parse_json_with_source_context( r#" { "import": "Inter", @@ -218,7 +219,7 @@ mod tests { #[test] fn test_default_values_when_no_arguments() -> Result<()> { - let data: IndexMap = serde_json::from_str( + let data: IndexMap = parse_json_with_source_context( r#" { "ABeeZee": { @@ -229,7 +230,7 @@ mod tests { "#, )?; - let request: NextFontRequest = serde_json::from_str( + let request: NextFontRequest = parse_json_with_source_context( r#" { "import": "ABeeZee", @@ -261,7 +262,7 @@ mod tests { #[test] fn test_errors_when_no_weights_chosen_no_variable() -> Result<()> { - let data: IndexMap = serde_json::from_str( + let data: IndexMap = parse_json_with_source_context( r#" { "ABeeZee": { @@ -272,7 +273,7 @@ mod tests { "#, )?; - let request: NextFontRequest = serde_json::from_str( + let request: NextFontRequest = parse_json_with_source_context( r#" { "import": "ABeeZee", @@ -297,7 +298,7 @@ mod tests { #[test] fn test_errors_on_unnecessary_weights() -> Result<()> { - let data: IndexMap = serde_json::from_str( + let data: IndexMap = parse_json_with_source_context( r#" { "ABeeZee": { @@ -308,7 +309,7 @@ mod tests { "#, )?; - let request: NextFontRequest = serde_json::from_str( + let request: NextFontRequest = parse_json_with_source_context( r#" { "import": "ABeeZee", @@ -336,7 +337,7 @@ mod tests { #[test] fn test_errors_on_unvavailable_weights() -> Result<()> { - let data: IndexMap = serde_json::from_str( + let data: IndexMap = parse_json_with_source_context( r#" { "ABeeZee": { @@ -347,7 +348,7 @@ mod tests { "#, )?; - let request: NextFontRequest = serde_json::from_str( + let request: NextFontRequest = parse_json_with_source_context( r#" { "import": "ABeeZee", @@ -374,7 +375,7 @@ mod tests { #[test] fn test_defaults_to_only_style_when_one_available() -> Result<()> { - let data: IndexMap = serde_json::from_str( + let data: IndexMap = parse_json_with_source_context( r#" { "ABeeZee": { @@ -385,7 +386,7 @@ mod tests { "#, )?; - let request: NextFontRequest = serde_json::from_str( + let request: NextFontRequest = parse_json_with_source_context( r#" { "import": "ABeeZee", @@ -406,7 +407,7 @@ mod tests { #[test] fn test_defaults_to_normal_style_when_multiple() -> Result<()> { - let data: IndexMap = serde_json::from_str( + let data: IndexMap = parse_json_with_source_context( r#" { "ABeeZee": { @@ -417,7 +418,7 @@ mod tests { "#, )?; - let request: NextFontRequest = serde_json::from_str( + let request: NextFontRequest = parse_json_with_source_context( r#" { "import": "ABeeZee", @@ -438,7 +439,7 @@ mod tests { #[test] fn test_errors_on_unknown_styles() -> Result<()> { - let data: IndexMap = serde_json::from_str( + let data: IndexMap = parse_json_with_source_context( r#" { "ABeeZee": { @@ -449,7 +450,7 @@ mod tests { "#, )?; - let request: NextFontRequest = serde_json::from_str( + let request: NextFontRequest = parse_json_with_source_context( r#" { "import": "ABeeZee", @@ -478,7 +479,7 @@ mod tests { #[test] fn test_errors_on_unknown_display() -> Result<()> { - let data: IndexMap = serde_json::from_str( + let data: IndexMap = parse_json_with_source_context( r#" { "ABeeZee": { @@ -489,7 +490,7 @@ mod tests { "#, )?; - let request: NextFontRequest = serde_json::from_str( + let request: NextFontRequest = parse_json_with_source_context( r#" { "import": "ABeeZee", @@ -519,7 +520,7 @@ mod tests { #[test] fn test_errors_on_axes_without_variable() -> Result<()> { - let data: IndexMap = serde_json::from_str( + let data: IndexMap = parse_json_with_source_context( r#" { "ABeeZee": { @@ -530,7 +531,7 @@ mod tests { "#, )?; - let request: NextFontRequest = serde_json::from_str( + let request: NextFontRequest = parse_json_with_source_context( r#" { "import": "ABeeZee", diff --git a/crates/next-core/src/next_font_google/util.rs b/crates/next-core/src/next_font_google/util.rs index 90244412c9a72b..1426ebd51d1c05 100644 --- a/crates/next-core/src/next_font_google/util.rs +++ b/crates/next-core/src/next_font_google/util.rs @@ -214,6 +214,7 @@ pub(crate) fn get_stylesheet_url( mod tests { use anyhow::Result; use indexmap::indexset; + use turbo_tasks_fs::json::parse_json_with_source_context; use super::get_font_axes; use crate::next_font_google::{ @@ -224,7 +225,7 @@ mod tests { #[test] fn test_errors_on_unknown_font() -> Result<()> { - let data: FontData = serde_json::from_str( + let data: FontData = parse_json_with_source_context( r#" { "ABeeZee": { @@ -252,7 +253,7 @@ mod tests { #[test] fn test_errors_on_missing_axes() -> Result<()> { - let data: FontData = serde_json::from_str( + let data: FontData = parse_json_with_source_context( r#" { "ABeeZee": { @@ -280,7 +281,7 @@ mod tests { #[test] fn test_selecting_axes() -> Result<()> { - let data: FontData = serde_json::from_str( + let data: FontData = parse_json_with_source_context( r#" { "Inter": { @@ -327,7 +328,7 @@ mod tests { #[test] fn test_no_wght_axis() -> Result<()> { - let data: FontData = serde_json::from_str( + let data: FontData = parse_json_with_source_context( r#" { "Inter": { @@ -368,7 +369,7 @@ mod tests { #[test] fn test_no_variable() -> Result<()> { - let data: FontData = serde_json::from_str( + let data: FontData = parse_json_with_source_context( r#" { "Hind": { diff --git a/crates/next-core/src/router.rs b/crates/next-core/src/router.rs index a129b8b9e383f6..1280f6f04f8ef9 100644 --- a/crates/next-core/src/router.rs +++ b/crates/next-core/src/router.rs @@ -4,7 +4,7 @@ use turbo_tasks::{ primitives::{JsonValueVc, StringsVc}, Value, }; -use turbo_tasks_fs::{to_sys_path, FileSystemPathVc}; +use turbo_tasks_fs::{json::parse_json_rope_with_source_context, to_sys_path, FileSystemPathVc}; use turbopack::evaluate_context::node_evaluate_asset_context; use turbopack_core::{ asset::AssetVc, @@ -187,7 +187,7 @@ pub async fn route( match &*result { JavaScriptValue::Value(val) => { - let result: RouterIncomingMessage = serde_json::from_reader(val.read())?; + let result: RouterIncomingMessage = parse_json_rope_with_source_context(val)?; Ok(RouterResult::from(result).cell()) } JavaScriptValue::Error => Ok(RouterResult::Error.cell()), diff --git a/crates/turbo-tasks-fs/Cargo.toml b/crates/turbo-tasks-fs/Cargo.toml index 821a7d48f13ce4..6f9aa23ce06e38 100644 --- a/crates/turbo-tasks-fs/Cargo.toml +++ b/crates/turbo-tasks-fs/Cargo.toml @@ -27,6 +27,7 @@ notify = "4.0.17" parking_lot = "0.12.1" serde = { version = "1.0.136", features = ["rc"] } serde_json = "1.0.85" +serde_path_to_error = "0.1.9" tokio = "1.21.2" turbo-tasks = { path = "../turbo-tasks" } turbo-tasks-hash = { path = "../turbo-tasks-hash" } diff --git a/crates/turbo-tasks-fs/src/json.rs b/crates/turbo-tasks-fs/src/json.rs new file mode 100644 index 00000000000000..9b8c85cdd63ddd --- /dev/null +++ b/crates/turbo-tasks-fs/src/json.rs @@ -0,0 +1,122 @@ +use std::{ + borrow::Cow, + fmt::{Display, Formatter, Write}, +}; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use turbo_tasks::trace::TraceRawVcs; + +use crate::{rope::Rope, source_context::get_source_context}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, TraceRawVcs)] +pub struct UnparseableJson { + #[turbo_tasks(trace_ignore)] + pub message: Cow<'static, str>, + pub path: Option, + /// The start line and column of the error. + /// Line and column is 0-based. + pub start_location: Option<(usize, usize)>, + /// The end line and column of the error. + /// Line and column is 0-based. + pub end_location: Option<(usize, usize)>, +} + +/// Converts a byte position to a 0-based line and column. +fn byte_to_location(pos: usize, text: &str) -> (usize, usize) { + let text = &text[..pos]; + let mut lines = text.lines().rev(); + let last = lines.next().unwrap_or(""); + let column = last.len(); + let line = lines.count(); + (line, column) +} + +impl UnparseableJson { + pub fn from_jsonc_error(e: jsonc_parser::errors::ParseError, text: &str) -> Self { + Self { + message: e.message.clone().into(), + path: None, + start_location: Some(byte_to_location(e.range.start, text)), + end_location: Some(byte_to_location(e.range.end, text)), + } + } + + pub fn from_serde_path_to_error(e: serde_path_to_error::Error) -> Self { + let inner = e.inner(); + Self { + message: inner.to_string().into(), + path: Some(e.path().to_string()), + start_location: Some((inner.line() - 1, inner.column())), + end_location: None, + } + } + + pub fn write_with_content(&self, writer: &mut impl Write, text: &str) -> std::fmt::Result { + writeln!(writer, "{}", self.message)?; + if let Some(path) = &self.path { + writeln!(writer, " at {}", path)?; + } + match (self.start_location, self.end_location) { + (Some((line, column)), Some((end_line, end_column))) => { + write!( + writer, + "{}", + get_source_context(text.lines(), line, column, end_line, end_column,) + )?; + } + (Some((line, column)), None) | (None, Some((line, column))) => { + write!( + writer, + "{}", + get_source_context(text.lines(), line, column, line, column) + )?; + } + (None, None) => { + write!(writer, "{}", get_source_context(text.lines(), 0, 0, 0, 0))?; + } + } + Ok(()) + } + + pub fn to_string_with_content(&self, text: &str) -> String { + let mut result = String::new(); + self.write_with_content(&mut result, text).unwrap(); + result + } +} + +impl Display for UnparseableJson { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message)?; + if let Some(path) = &self.path { + write!(f, " at {}", path)?; + } + Ok(()) + } +} + +pub fn parse_json_with_source_context<'de, T: Deserialize<'de>>(text: &'de str) -> Result { + let de = &mut serde_json::Deserializer::from_str(text); + match serde_path_to_error::deserialize(de) { + Ok(data) => Ok(data), + Err(e) => { + return Err(anyhow::Error::msg( + UnparseableJson::from_serde_path_to_error(e).to_string_with_content(text), + )) + } + } +} + +pub fn parse_json_rope_with_source_context<'de, T: Deserialize<'de>>(rope: &'de Rope) -> Result { + let de = &mut serde_json::Deserializer::from_reader(rope.read()); + match serde_path_to_error::deserialize(de) { + Ok(data) => Ok(data), + Err(e) => { + let cow = rope.to_str()?; + return Err(anyhow::Error::msg( + UnparseableJson::from_serde_path_to_error(e).to_string_with_content(&cow), + )); + } + } +} diff --git a/crates/turbo-tasks-fs/src/lib.rs b/crates/turbo-tasks-fs/src/lib.rs index 15a34f48d763b0..6b865b06695395 100644 --- a/crates/turbo-tasks-fs/src/lib.rs +++ b/crates/turbo-tasks-fs/src/lib.rs @@ -4,18 +4,23 @@ #![feature(iter_advance_by)] #![feature(io_error_more)] #![feature(main_separator_str)] +#![feature(box_syntax)] +#![feature(round_char_boundary)] pub mod attach; pub mod embed; pub mod glob; mod invalidator_map; +pub mod json; mod mutex_map; mod read_glob; mod retry; pub mod rope; +pub mod source_context; pub mod util; use std::{ + borrow::Cow, collections::{HashMap, HashSet}, fmt::{self, Debug, Display, Formatter}, fs::FileType, @@ -52,7 +57,7 @@ use turbo_tasks::{ use turbo_tasks_hash::hash_xxh3_hash64; use util::{join_path, normalize_path, sys_to_unix, unix_to_sys}; -use self::mutex_map::MutexMap; +use self::{json::UnparseableJson, mutex_map::MutexMap}; #[cfg(target_family = "windows")] use crate::util::is_windows_raw_path; use crate::{ @@ -1403,10 +1408,15 @@ impl FileContent { pub fn parse_json(&self) -> FileJsonContent { match self { - FileContent::Content(file) => match serde_json::from_reader(file.read()) { - Ok(data) => FileJsonContent::Content(data), - Err(_) => FileJsonContent::Unparseable, - }, + FileContent::Content(file) => { + let de = &mut serde_json::Deserializer::from_reader(file.read()); + match serde_path_to_error::deserialize(de) { + Ok(data) => FileJsonContent::Content(data), + Err(e) => FileJsonContent::Unparseable( + box UnparseableJson::from_serde_path_to_error(e), + ), + } + } FileContent::NotFound => FileJsonContent::NotFound, } } @@ -1424,11 +1434,16 @@ impl FileContent { ) { Ok(data) => match data { Some(value) => FileJsonContent::Content(value), - None => FileJsonContent::Unparseable, + None => FileJsonContent::unparseable( + "text content doesn't contain any json data", + ), }, - Err(_) => FileJsonContent::Unparseable, + Err(e) => FileJsonContent::Unparseable(box UnparseableJson::from_jsonc_error( + e, + string.as_ref(), + )), }, - Err(_) => FileJsonContent::Unparseable, + Err(_) => FileJsonContent::unparseable("binary is not valid utf-8 text"), }, FileContent::NotFound => FileJsonContent::NotFound, } @@ -1483,7 +1498,7 @@ impl FileContentVc { #[turbo_tasks::value(shared, serialization = "none")] pub enum FileJsonContent { Content(Value), - Unparseable, + Unparseable(Box), NotFound, } @@ -1497,12 +1512,32 @@ impl ValueToString for FileJsonContent { async fn to_string(&self) -> Result { match self { FileJsonContent::Content(json) => Ok(StringVc::cell(json.to_string())), - FileJsonContent::Unparseable => Err(anyhow!("File is not valid JSON")), + FileJsonContent::Unparseable(e) => Err(anyhow!("File is not valid JSON: {}", e)), FileJsonContent::NotFound => Err(anyhow!("File not found")), } } } +impl FileJsonContent { + pub fn unparseable(message: &'static str) -> Self { + FileJsonContent::Unparseable(Box::new(UnparseableJson { + message: Cow::Borrowed(message), + path: None, + start_location: None, + end_location: None, + })) + } + + pub fn unparseable_with_message(message: Cow<'static, str>) -> Self { + FileJsonContent::Unparseable(Box::new(UnparseableJson { + message, + path: None, + start_location: None, + end_location: None, + })) + } +} + #[derive(Debug, PartialEq, Eq)] pub struct FileLine { pub content: String, diff --git a/crates/turbo-tasks-fs/src/source_context.rs b/crates/turbo-tasks-fs/src/source_context.rs new file mode 100644 index 00000000000000..55410ada2983ae --- /dev/null +++ b/crates/turbo-tasks-fs/src/source_context.rs @@ -0,0 +1,177 @@ +use std::{borrow::Cow, cmp::Ordering, fmt::Display}; + +pub enum SourceContextLine<'a> { + Context { + line: usize, + outside: Cow<'a, str>, + }, + Start { + line: usize, + before: Cow<'a, str>, + inside: Cow<'a, str>, + }, + End { + line: usize, + inside: Cow<'a, str>, + after: Cow<'a, str>, + }, + StartAndEnd { + line: usize, + before: Cow<'a, str>, + inside: Cow<'a, str>, + after: Cow<'a, str>, + }, + Inside { + line: usize, + inside: Cow<'a, str>, + }, +} + +impl<'a> Display for SourceContextLine<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SourceContextLine::Context { line, outside } => { + writeln!(f, "{line:>6} | {outside}") + } + SourceContextLine::Start { + line, + before, + inside, + } => { + writeln!( + f, + " | {}v{}", + " ".repeat(before.len()), + "-".repeat(inside.len()), + )?; + writeln!(f, "{line:>6} + {before}{inside}") + } + SourceContextLine::End { + line, + inside, + after, + } => { + writeln!(f, "{line:>6} + {inside}{after}")?; + writeln!(f, " +{}^", "-".repeat(inside.len())) + } + SourceContextLine::StartAndEnd { + line, + before, + inside, + after, + } => { + writeln!(f, "{line:>6} | {before}{inside}{after}")?; + if inside.len() >= 2 { + writeln!( + f, + " + {}^{}^", + " ".repeat(before.len()), + "-".repeat(inside.len() - 2), + ) + } else { + writeln!(f, " | {}^", " ".repeat(before.len()),) + } + } + SourceContextLine::Inside { line, inside } => { + writeln!(f, "{line:>6} + {inside}") + } + } + } +} + +pub struct SourceContextLines<'a>(pub Vec>); + +impl<'a> Display for SourceContextLines<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for line in &self.0 { + write!(f, "{}", line)?; + } + Ok(()) + } +} + +/// Compute the source context for a given range of lines, including selected +/// ranges in these lines. (Lines are 0-indexed) +pub fn get_source_context<'a>( + lines: impl Iterator, + start_line: usize, + start_column: usize, + end_line: usize, + end_column: usize, +) -> SourceContextLines<'a> { + let mut result = Vec::new(); + let context_start = start_line.saturating_sub(4); + let context_end = end_line + 4; + for (i, l) in lines.enumerate().take(context_end + 1).skip(context_start) { + let n = i + 1; + fn safe_split_at(s: &str, i: usize) -> (&str, &str) { + if i < s.len() { + s.split_at(s.floor_char_boundary(i)) + } else { + (s, "") + } + } + fn limit_len(s: &str) -> Cow<'_, str> { + if s.len() < 200 { + return Cow::Borrowed(s); + } + let (a, b) = s.split_at(s.floor_char_boundary(98)); + let (_, c) = b.split_at(b.ceil_char_boundary(b.len() - 99)); + Cow::Owned(format!("{}...{}", a, c)) + } + match (i.cmp(&start_line), i.cmp(&end_line)) { + // outside + (Ordering::Less, _) | (_, Ordering::Greater) => { + result.push(SourceContextLine::Context { + line: n, + outside: limit_len(l), + }); + } + // start line + (Ordering::Equal, Ordering::Less) => { + let (before, inside) = safe_split_at(l, start_column); + let before = limit_len(before); + let inside = limit_len(inside); + result.push(SourceContextLine::Start { + line: n, + before, + inside, + }); + } + // start and end line + (Ordering::Equal, Ordering::Equal) => { + let real_start = l.floor_char_boundary(start_column); + let (before, temp) = safe_split_at(l, real_start); + let (inside, after) = safe_split_at(temp, end_column - real_start); + let before = limit_len(before); + let inside = limit_len(inside); + let after = limit_len(after); + result.push(SourceContextLine::StartAndEnd { + line: n, + before, + inside, + after, + }); + } + // end line + (Ordering::Greater, Ordering::Equal) => { + let (inside, after) = safe_split_at(l, end_column); + let inside = limit_len(inside); + let after = limit_len(after); + result.push(SourceContextLine::End { + line: n, + inside, + after, + }); + } + // middle line + (Ordering::Greater, Ordering::Less) => { + result.push(SourceContextLine::Inside { + line: n, + inside: limit_len(l), + }); + } + } + } + SourceContextLines(result) +} diff --git a/crates/turbopack-cli-utils/src/issue.rs b/crates/turbopack-cli-utils/src/issue.rs index 33c5902b7a6f3e..9eef99d3f7c907 100644 --- a/crates/turbopack-cli-utils/src/issue.rs +++ b/crates/turbopack-cli-utils/src/issue.rs @@ -1,6 +1,5 @@ use std::{ - borrow::Cow, - cmp::{min, Ordering}, + cmp::min, collections::{hash_map::Entry, HashMap, HashSet}, fmt::Write as _, path::PathBuf, @@ -13,7 +12,9 @@ use crossterm::style::{StyledContent, Stylize}; use owo_colors::{OwoColorize as _, Style}; use turbo_tasks::{RawVc, TransientValue, TryJoinIterExt, ValueToString}; use turbo_tasks_fs::{ - attach::AttachedFileSystemVc, to_sys_path, FileLinesContent, FileSystemPathVc, + attach::AttachedFileSystemVc, + source_context::{get_source_context, SourceContextLine}, + to_sys_path, FileLinesContent, FileSystemPathVc, }; use turbopack_core::issue::{ Issue, IssueProcessingPathItem, IssueSeverity, IssueVc, OptionIssueProcessingPathItemsVc, @@ -79,84 +80,77 @@ fn severity_to_style(severity: IssueSeverity) -> Style { fn format_source_content(source: &PlainIssueSource, formatted_issue: &mut String) { if let FileLinesContent::Lines(lines) = source.asset.content.lines() { - let context_start = source.start.line.saturating_sub(4); - let context_end = source.end.line + 4; - for (i, l) in lines - .iter() - .map(|l| &l.content) - .enumerate() - .take(context_end + 1) - .skip(context_start) - { - let n = i + 1; - fn safe_split_at(s: &str, i: usize) -> (&str, &str) { - if i < s.len() { - s.split_at(s.floor_char_boundary(i)) - } else { - (s, "") - } - } - fn limit_len(s: &str) -> Cow<'_, str> { - if s.len() < 200 { - return Cow::Borrowed(s); + let start_line = source.start.line; + let end_line = source.end.line; + let start_column = source.start.column; + let end_column = source.end.column; + let lines = lines.iter().map(|l| l.content.as_str()); + let ctx = get_source_context(lines, start_line, start_column, end_line, end_column); + let f = formatted_issue; + for line in ctx.0 { + match line { + SourceContextLine::Context { line, outside } => { + writeln!(f, "{}", format_args!("{line:>6} | {outside}").dimmed()).unwrap(); } - let (a, b) = s.split_at(s.floor_char_boundary(98)); - let (_, c) = b.split_at(b.ceil_char_boundary(b.len() - 99)); - Cow::Owned(format!("{}...{}", a, c)) - } - match (i.cmp(&source.start.line), i.cmp(&source.end.line)) { - // outside - (Ordering::Less, _) | (_, Ordering::Greater) => { + SourceContextLine::Start { + line, + before, + inside, + } => { writeln!( - formatted_issue, - "{:>6} {}", - n.dimmed(), - limit_len(l).dimmed() + f, + " | {}{}{}", + " ".repeat(before.len()), + "v".bold(), + "-".repeat(inside.len()).bold(), ) .unwrap(); + writeln!(f, "{line:>6} + {}{}", before.dimmed(), inside.bold()).unwrap(); } - // start line - (Ordering::Equal, Ordering::Less) => { - let (before, marked) = safe_split_at(l, source.start.column); + SourceContextLine::End { + line, + inside, + after, + } => { + writeln!(f, "{line:>6} + {}{}", inside.bold(), after.dimmed()).unwrap(); writeln!( - formatted_issue, - "{:>6} + {}{}", - n, - limit_len(before).dimmed(), - limit_len(marked).bold() + f, + " +{}{}", + "-".repeat(inside.len()).bold(), + "^".bold() ) .unwrap(); } - // start and end line - (Ordering::Equal, Ordering::Equal) => { - let real_start = l.floor_char_boundary(source.start.column); - let (before, temp) = safe_split_at(l, real_start); - let (middle, after) = safe_split_at(temp, source.end.column - real_start); + SourceContextLine::StartAndEnd { + line, + before, + inside, + after, + } => { writeln!( - formatted_issue, - "{:>6} > {}{}{}", - n, - limit_len(before).dimmed(), - limit_len(middle).bold(), - limit_len(after).dimmed() - ) - .unwrap(); - } - // end line - (Ordering::Greater, Ordering::Equal) => { - let (marked, after) = safe_split_at(l, source.end.column); - writeln!( - formatted_issue, - "{:>6} + {}{}", - n, - limit_len(marked).bold(), - limit_len(after).dimmed() + f, + "{line:>6} | {}{}{}", + before.dimmed(), + inside.bold(), + after.dimmed() ) .unwrap(); + if inside.len() >= 2 { + writeln!( + f, + " + {}{}{}{}", + " ".repeat(before.len()), + "^".bold(), + "-".repeat(inside.len() - 2).bold(), + "^".bold(), + ) + .unwrap(); + } else { + writeln!(f, " | {}{}", " ".repeat(before.len()), "^".bold()).unwrap(); + } } - // middle line - (Ordering::Greater, Ordering::Less) => { - writeln!(formatted_issue, "{:>6} | {}", n, limit_len(l).bold()).unwrap() + SourceContextLine::Inside { line, inside } => { + writeln!(f, "{:>6} + {}", line.bold(), inside.bold()).unwrap(); } } } diff --git a/crates/turbopack-core/src/asset.rs b/crates/turbopack-core/src/asset.rs index 7ee4755c3c793f..ca88781d378571 100644 --- a/crates/turbopack-core/src/asset.rs +++ b/crates/turbopack-core/src/asset.rs @@ -91,7 +91,18 @@ impl AssetContentVc { let this = self.await?; match &*this { AssetContent::File(content) => Ok(content.parse_json()), - AssetContent::Redirect { .. } => Ok(FileJsonContent::Unparseable.cell()), + AssetContent::Redirect { .. } => { + Ok(FileJsonContent::unparseable("a redirect can't be parsed as json").cell()) + } + } + } + + #[turbo_tasks::function] + pub async fn file_content(self) -> Result { + let this = self.await?; + match &*this { + AssetContent::File(content) => Ok(*content), + AssetContent::Redirect { .. } => Ok(FileContent::NotFound.cell()), } } @@ -109,7 +120,9 @@ impl AssetContentVc { let this = self.await?; match &*this { AssetContent::File(content) => Ok(content.parse_json_with_comments()), - AssetContent::Redirect { .. } => Ok(FileJsonContent::Unparseable.cell()), + AssetContent::Redirect { .. } => { + Ok(FileJsonContent::unparseable("a redirect can't be parsed as json").cell()) + } } } diff --git a/crates/turbopack-dev-server/src/introspect/mod.rs b/crates/turbopack-dev-server/src/introspect/mod.rs index eba39629694a58..c668989330e258 100644 --- a/crates/turbopack-dev-server/src/introspect/mod.rs +++ b/crates/turbopack-dev-server/src/introspect/mod.rs @@ -2,7 +2,7 @@ use std::{collections::HashSet, fmt::Display}; use anyhow::Result; use turbo_tasks::{primitives::StringVc, TryJoinIterExt}; -use turbo_tasks_fs::{File, FileContent}; +use turbo_tasks_fs::{json::parse_json_with_source_context, File, FileContent}; use turbopack_core::{ asset::AssetContent, introspect::{Introspectable, IntrospectableChildrenVc, IntrospectableVc}, @@ -87,7 +87,7 @@ impl ContentSource for IntrospectionSource { self_vc.as_introspectable() } } else { - serde_json::from_str(path)? + parse_json_with_source_context(path)? }; let ty = introspectable.ty().await?; let title = introspectable.title().await?; diff --git a/crates/turbopack-dev-server/src/update/server.rs b/crates/turbopack-dev-server/src/update/server.rs index e7ef3a8fd0f8ea..2ff147f7fca5c3 100644 --- a/crates/turbopack-dev-server/src/update/server.rs +++ b/crates/turbopack-dev-server/src/update/server.rs @@ -11,6 +11,7 @@ use pin_project_lite::pin_project; use tokio::select; use tokio_stream::StreamMap; use turbo_tasks::{TransientInstance, TurboTasksApi}; +use turbo_tasks_fs::json::parse_json_with_source_context; use turbopack_cli_utils::issue::ConsoleUiVc; use turbopack_core::version::Update; @@ -185,12 +186,11 @@ impl Stream for UpdateClient { } }; - match serde_json::from_str(&msg) { + match parse_json_with_source_context(&msg).context("deserializing websocket message") { Ok(msg) => Poll::Ready(Some(Ok(msg))), Err(err) => { *this.ended = true; - let err = Error::new(err).context("deserializing websocket message"); Poll::Ready(Some(Err(err))) } } diff --git a/crates/turbopack-ecmascript/src/resolve/node_native_binding.rs b/crates/turbopack-ecmascript/src/resolve/node_native_binding.rs index 2400ed2a292ffa..2aa7618e0a926a 100644 --- a/crates/turbopack-ecmascript/src/resolve/node_native_binding.rs +++ b/crates/turbopack-ecmascript/src/resolve/node_native_binding.rs @@ -4,7 +4,10 @@ use lazy_static::lazy_static; use regex::Regex; use serde::{Deserialize, Serialize}; use turbo_tasks::{primitives::StringVc, ValueToString, ValueToStringVc}; -use turbo_tasks_fs::{glob::GlobVc, DirectoryEntry, FileContent, FileSystemPathVc}; +use turbo_tasks_fs::{ + glob::GlobVc, json::parse_json_rope_with_source_context, DirectoryEntry, FileContent, + FileSystemPathVc, +}; use turbopack_core::{ asset::{Asset, AssetContent, AssetVc}, reference::{AssetReference, AssetReferenceVc}, @@ -100,7 +103,7 @@ pub async fn resolve_node_pre_gyp_files( let config_file_path = config_path.path(); let config_file_dir = config_file_path.parent(); let node_pre_gyp_config: NodePreGypConfigJson = - serde_json::from_reader(config_file.read())?; + parse_json_rope_with_source_context(config_file.content())?; let mut assets: IndexSet = IndexSet::new(); for version in node_pre_gyp_config.binary.napi_versions.iter() { let native_binding_path = NAPI_VERSION_TEMPLATE.replace( diff --git a/crates/turbopack-ecmascript/src/transform/mod.rs b/crates/turbopack-ecmascript/src/transform/mod.rs index 031280e150b3d4..9ad055538d159d 100644 --- a/crates/turbopack-ecmascript/src/transform/mod.rs +++ b/crates/turbopack-ecmascript/src/transform/mod.rs @@ -24,7 +24,7 @@ use turbo_tasks::{ primitives::{StringVc, StringsVc}, trace::TraceRawVcs, }; -use turbo_tasks_fs::FileSystemPathVc; +use turbo_tasks_fs::{json::parse_json_with_source_context, FileSystemPathVc}; use turbopack_core::environment::EnvironmentVc; use self::server_to_client_proxy::{create_proxy_module, is_client_module}; @@ -186,7 +186,7 @@ impl EcmascriptInputTransform { program.visit_mut_with(&mut styled_components::styled_components( FileName::Anon, file_name_hash, - serde_json::from_str("{}")?, + parse_json_with_source_context("{}")?, )); } EcmascriptInputTransform::StyledJsx => { diff --git a/crates/turbopack-ecmascript/src/typescript/mod.rs b/crates/turbopack-ecmascript/src/typescript/mod.rs index a24162688f5ec1..a9cf3c1d1e1c9c 100644 --- a/crates/turbopack-ecmascript/src/typescript/mod.rs +++ b/crates/turbopack-ecmascript/src/typescript/mod.rs @@ -49,7 +49,7 @@ impl Asset for TsConfigModuleAsset { async fn references(&self) -> Result { let mut references = Vec::new(); let configs = read_tsconfigs( - self.source.content().parse_json_with_comments(), + self.source.content().file_content(), self.source, apply_cjs_specific_options(self.origin.resolve_options(Value::new( ReferenceType::CommonJs(CommonJsReferenceSubType::Undefined), diff --git a/crates/turbopack-ecmascript/src/typescript/resolve.rs b/crates/turbopack-ecmascript/src/typescript/resolve.rs index fc2b7477e31dac..940ce9101cd9b8 100644 --- a/crates/turbopack-ecmascript/src/typescript/resolve.rs +++ b/crates/turbopack-ecmascript/src/typescript/resolve.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Write}; use anyhow::Result; use serde_json::Value as JsonValue; @@ -6,7 +6,9 @@ use turbo_tasks::{ primitives::{StringVc, StringsVc}, Value, ValueToString, ValueToStringVc, }; -use turbo_tasks_fs::{FileJsonContent, FileJsonContentVc, FileSystemPathVc}; +use turbo_tasks_fs::{ + FileContent, FileContentVc, FileJsonContent, FileJsonContentVc, FileSystemPathVc, +}; use turbopack_core::{ asset::{Asset, AssetVc}, context::AssetContext, @@ -35,18 +37,26 @@ pub struct TsConfigIssue { } pub async fn read_tsconfigs( - mut data: FileJsonContentVc, + mut data: FileContentVc, mut tsconfig: AssetVc, resolve_options: ResolveOptionsVc, ) -> Result> { let mut configs = Vec::new(); loop { - match &*data.await? { - FileJsonContent::Unparseable => { + let parsed_data = data.parse_json_with_comments(); + match &*parsed_data.await? { + FileJsonContent::Unparseable(e) => { + let mut message = "tsconfig is not parseable: invalid JSON: ".to_string(); + if let FileContent::Content(content) = &*data.await? { + let text = content.content().to_str()?; + e.write_with_content(&mut message, text.as_ref())?; + } else { + write!(message, "{}", e)?; + } TsConfigIssue { severity: IssueSeverity::Error.into(), path: tsconfig.path(), - message: StringVc::cell("tsconfig is not parseable: invalid JSON".into()), + message: StringVc::cell(message), } .cell() .as_issue() @@ -65,7 +75,7 @@ pub async fn read_tsconfigs( break; } FileJsonContent::Content(json) => { - configs.push((data, tsconfig)); + configs.push((parsed_data, tsconfig)); if let Some(extends) = json["extends"].as_str() { let context = tsconfig.path().parent(); let result = resolve( @@ -79,7 +89,7 @@ pub async fn read_tsconfigs( // "some/path/node_modules/xyz/abc.json" and "some/node_modules/xyz/abc.json". // We only want to use the first one. if let Some(&asset) = result.iter().next() { - data = asset.content().parse_json_with_comments(); + data = asset.content().file_content(); tsconfig = asset; } else { TsConfigIssue { @@ -136,7 +146,7 @@ pub async fn tsconfig_resolve_options( resolve_in_tsconfig_options: ResolveOptionsVc, ) -> Result { let configs = read_tsconfigs( - tsconfig.read().parse_json_with_comments(), + tsconfig.read(), SourceAssetVc::new(tsconfig).into(), resolve_in_tsconfig_options, ) diff --git a/crates/turbopack-json/src/lib.rs b/crates/turbopack-json/src/lib.rs index a9acbf141310f3..689a8a5ccde877 100644 --- a/crates/turbopack-json/src/lib.rs +++ b/crates/turbopack-json/src/lib.rs @@ -7,9 +7,11 @@ #![feature(min_specialization)] -use anyhow::{Context, Result}; +use std::fmt::Write; + +use anyhow::{bail, Error, Result}; use turbo_tasks::{primitives::StringVc, ValueToString, ValueToStringVc}; -use turbo_tasks_fs::FileSystemPathVc; +use turbo_tasks_fs::{FileContent, FileJsonContent, FileSystemPathVc}; use turbopack_core::{ asset::{Asset, AssetContentVc, AssetVc}, chunk::{ChunkItem, ChunkItemVc, ChunkVc, ChunkableAsset, ChunkableAssetVc, ChunkingContextVc}, @@ -116,20 +118,36 @@ impl EcmascriptChunkItem for JsonChunkItem { async fn content(&self) -> Result { // We parse to JSON and then stringify again to ensure that the // JSON is valid. - let content = self - .module - .path() - .read_json() - .to_string() - .await - .context("Unable to make a module from invalid JSON")?; - let js_str_content = serde_json::to_string(content.as_str())?; - let inner_code = format!("__turbopack_export_value__(JSON.parse({js_str_content}));"); - Ok(EcmascriptChunkItemContent { - inner_code: inner_code.into(), - ..Default::default() + let content = self.module.path().read(); + let data = content.parse_json().await?; + match &*data { + FileJsonContent::Content(data) => { + let js_str_content = serde_json::to_string(data)?; + let inner_code = + format!("__turbopack_export_value__(JSON.parse({js_str_content}));"); + Ok(EcmascriptChunkItemContent { + inner_code: inner_code.into(), + ..Default::default() + } + .into()) + } + FileJsonContent::Unparseable(e) => { + let mut message = "Unable to make a module from invalid JSON: ".to_string(); + if let FileContent::Content(content) = &*content.await? { + let text = content.content().to_str()?; + e.write_with_content(&mut message, text.as_ref())?; + } else { + write!(message, "{}", e)?; + } + return Err(Error::msg(message)); + } + FileJsonContent::NotFound => { + bail!( + "JSON file not found: {}", + self.module.path().to_string().await? + ); + } } - .into()) } } diff --git a/crates/turbopack-node/src/transforms/postcss.rs b/crates/turbopack-node/src/transforms/postcss.rs index 9fb5e9a77f35f2..0ce56c783b8fd1 100644 --- a/crates/turbopack-node/src/transforms/postcss.rs +++ b/crates/turbopack-node/src/transforms/postcss.rs @@ -6,7 +6,10 @@ use turbo_tasks::{ primitives::{JsonValueVc, StringsVc}, TryJoinIterExt, Value, }; -use turbo_tasks_fs::{File, FileContent, FileSystemEntryType, FileSystemPathVc}; +use turbo_tasks_fs::{ + json::parse_json_rope_with_source_context, File, FileContent, FileSystemEntryType, + FileSystemPathVc, +}; use turbopack_core::{ asset::{Asset, AssetContent, AssetContentVc, AssetVc}, context::{AssetContext, AssetContextVc}, @@ -235,7 +238,7 @@ impl PostCssTransformedAssetVc { assets: Vec::new() }.cell()); }; - let processed_css: PostCssProcessingResult = serde_json::from_reader(val.read()) + let processed_css: PostCssProcessingResult = parse_json_rope_with_source_context(val) .context("Unable to deserializate response from PostCSS transform operation")?; // TODO handle SourceMap let file = File::from(processed_css.css); diff --git a/crates/turbopack-node/src/transforms/webpack.rs b/crates/turbopack-node/src/transforms/webpack.rs index d0a2c57dabcf58..abf2fb55b5fa34 100644 --- a/crates/turbopack-node/src/transforms/webpack.rs +++ b/crates/turbopack-node/src/transforms/webpack.rs @@ -2,7 +2,9 @@ use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; use serde_json::json; use turbo_tasks::{primitives::JsonValueVc, trace::TraceRawVcs, Value}; -use turbo_tasks_fs::{File, FileContent, FileSystemPathVc}; +use turbo_tasks_fs::{ + json::parse_json_rope_with_source_context, File, FileContent, FileSystemPathVc, +}; use turbopack_core::{ asset::{Asset, AssetContent, AssetContentVc, AssetVc}, context::{AssetContext, AssetContextVc}, @@ -178,7 +180,7 @@ impl WebpackLoadersProcessedAssetVc { assets: Vec::new() }.cell()); }; - let processed: WebpackLoadersProcessingResult = serde_json::from_reader(val.read()) + let processed: WebpackLoadersProcessingResult = parse_json_rope_with_source_context(val) .context("Unable to deserializate response from webpack loaders transform operation")?; // TODO handle SourceMap let file = File::from(processed.source); diff --git a/crates/turbopack-tests/tests/snapshot.rs b/crates/turbopack-tests/tests/snapshot.rs index 7ef4efd3731db0..e8f250be321fd4 100644 --- a/crates/turbopack-tests/tests/snapshot.rs +++ b/crates/turbopack-tests/tests/snapshot.rs @@ -14,8 +14,9 @@ use test_generator::test_resources; use turbo_tasks::{debug::ValueDebug, NothingVc, TryJoinIterExt, TurboTasks, Value, ValueToString}; use turbo_tasks_env::DotenvProcessEnvVc; use turbo_tasks_fs::{ - util::sys_to_unix, DirectoryContent, DirectoryEntry, DiskFileSystemVc, File, FileContent, - FileSystem, FileSystemEntryType, FileSystemPathVc, FileSystemVc, + json::parse_json_with_source_context, util::sys_to_unix, DirectoryContent, DirectoryEntry, + DiskFileSystemVc, File, FileContent, FileSystem, FileSystemEntryType, FileSystemPathVc, + FileSystemVc, }; use turbo_tasks_hash::encode_hex; use turbo_tasks_memory::MemoryBackend; @@ -128,7 +129,7 @@ async fn run_test(resource: String) -> Result { let options_file = fs::read_to_string(test_path.join("options.json")); let options = match options_file { Err(_) => SnapshotOptions::default(), - Ok(options_str) => serde_json::from_str(&options_str).unwrap(), + Ok(options_str) => parse_json_with_source_context(&options_str).unwrap(), }; let root_fs = DiskFileSystemVc::new("workspace".to_string(), WORKSPACE_ROOT.clone()); let project_fs = DiskFileSystemVc::new("project".to_string(), WORKSPACE_ROOT.clone()); diff --git a/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json b/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json index 61a60d9b14f921..1a09514f79ec10 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json +++ b/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json @@ -1,3 +1,5 @@ { - "this-is": "invalid" // lint-staged will remove trailing commas, so here's a comment + "nested": { + "this-is": "invalid" // lint-staged will remove trailing commas, so here's a comment + } } diff --git a/crates/turbopack-tests/tests/snapshot/imports/json/issues/Code generation for chunk item errored-1fdecc.txt b/crates/turbopack-tests/tests/snapshot/imports/json/issues/Code generation for chunk item errored-e32941.txt similarity index 64% rename from crates/turbopack-tests/tests/snapshot/imports/json/issues/Code generation for chunk item errored-1fdecc.txt rename to crates/turbopack-tests/tests/snapshot/imports/json/issues/Code generation for chunk item errored-e32941.txt index ee9fa760241d5c..7dafa3788edf73 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/json/issues/Code generation for chunk item errored-1fdecc.txt +++ b/crates/turbopack-tests/tests/snapshot/imports/json/issues/Code generation for chunk item errored-e32941.txt @@ -3,7 +3,7 @@ PlainIssue { context: "[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json", category: "code generation", title: "Code generation for chunk item errored", - description: "An error occurred while generating the chunk item [project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)\n at Execution of module_factory failed\n at Execution of JsonChunkItem::content failed\n at Unable to make a module from invalid JSON\n at Execution of FileJsonContent::to_string failed\n at File is not valid JSON", + description: "An error occurred while generating the chunk item [project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)\n at Execution of module_factory failed\n at Execution of JsonChunkItem::content failed\n at Unable to make a module from invalid JSON: expected `,` or `}` at line 3 column 26\n at nested.?\n at 2:26\n 1 | {\n 2 | \"nested\": {\n 3 | \"this-is\": \"invalid\" // lint-staged will remove trailing commas, so here's a comment\n | ^\n 4 | }\n 5 | }\n", detail: "", documentation_link: "", source: None, diff --git a/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_6aa119.js b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_6aa119.js index 510f01139efacd..a4240048a20671 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_6aa119.js +++ b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_6aa119.js @@ -13,11 +13,11 @@ console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$ })()), "[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/package.json (json)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { -__turbopack_export_value__(JSON.parse("{\"name\":\"json-snapshot\"}")); +__turbopack_export_value__(JSON.parse({"name":"json-snapshot"})); })()), "[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)": (() => {{ -throw new Error("An error occurred while generating the chunk item [project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)\n at Execution of module_factory failed\n at Execution of JsonChunkItem::content failed\n at Unable to make a module from invalid JSON\n at Execution of FileJsonContent::to_string failed\n at File is not valid JSON"); +throw new Error("An error occurred while generating the chunk item [project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)\n at Execution of module_factory failed\n at Execution of JsonChunkItem::content failed\n at Unable to make a module from invalid JSON: expected `,` or `}` at line 3 column 26\n at nested.?\n at 2:26\n 1 | {\n 2 | \"nested\": {\n 3 | \"this-is\": \"invalid\" // lint-staged will remove trailing commas, so here's a comment\n | ^\n 4 | }\n 5 | }\n"); }}), }, ({ loadedChunks, instantiateRuntimeModule }) => {