diff --git a/Cargo.lock b/Cargo.lock index 687083813aa4..7b0056f010ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2686,6 +2686,7 @@ dependencies = [ "supports-color", "supports-hyperlinks", "supports-unicode", + "syntect", "terminal_size 0.4.1", "textwrap", "thiserror 1.0.69", @@ -3068,6 +3069,28 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e296cf87e61c9cfc1a61c3c63a0f7f286ed4554e0e22be84e8a38e1d264a2a29" +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.9.1", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "oorandom" version = "11.1.4" @@ -3345,6 +3368,25 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +dependencies = [ + "base64", + "indexmap 2.7.1", + "quick-xml", + "serde", + "time", +] + [[package]] name = "pnp" version = "0.9.4" @@ -3564,6 +3606,15 @@ dependencies = [ "unicase", ] +[[package]] +name = "quick-xml" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.38" @@ -7183,6 +7234,28 @@ dependencies = [ "syn 2.0.95", ] +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax 0.8.5", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", + "walkdir", + "yaml-rust", +] + [[package]] name = "tap" version = "1.0.1" @@ -8770,6 +8843,15 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/crates/rspack_error/Cargo.toml b/crates/rspack_error/Cargo.toml index f48ccc4ba548..dd75cb249f45 100644 --- a/crates/rspack_error/Cargo.toml +++ b/crates/rspack_error/Cargo.toml @@ -12,7 +12,6 @@ anyhow = { workspace = true, features = ["backtrace"] } cow-utils = { workspace = true } derive_more = { workspace = true, features = ["debug"] } futures = { workspace = true } -miette = { workspace = true, features = ["fancy", "derive"] } once_cell = { workspace = true } owo-colors = { workspace = true } rspack_cacheable = { workspace = true } @@ -26,3 +25,9 @@ textwrap = { workspace = true } thiserror = { workspace = true } unicode-width = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +miette = { workspace = true, features = ["fancy", "derive"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +miette = { workspace = true, features = ["fancy", "derive", "syntect-highlighter"] } diff --git a/crates/rspack_error/src/diagnostic.rs b/crates/rspack_error/src/diagnostic.rs index 2061c34201c0..a14607574c18 100644 --- a/crates/rspack_error/src/diagnostic.rs +++ b/crates/rspack_error/src/diagnostic.rs @@ -1,4 +1,9 @@ -use std::{borrow::Cow, fmt, ops::Deref, sync::Arc}; +use std::{ + borrow::Cow, + fmt, + ops::Deref, + sync::{Arc, LazyLock}, +}; use cow_utils::CowUtils; use miette::{GraphicalTheme, IntoDiagnostic, MietteDiagnostic}; @@ -165,18 +170,31 @@ impl Diagnostic { } } +static COLORED_GRAPHICAL_REPORT_HANDLER: LazyLock = LazyLock::new(|| { + GraphicalReportHandler::new() + .with_theme(GraphicalTheme::unicode()) + .with_context_lines(2) + .with_width(usize::MAX) +}); + +static NO_COLOR_GRAPHICAL_REPORT_HANDLER: LazyLock = LazyLock::new(|| { + GraphicalReportHandler::new() + .with_theme(GraphicalTheme::unicode_nocolor()) + .with_context_lines(2) + .with_width(usize::MAX) + .without_syntax_highlighting() +}); + impl Diagnostic { pub fn render_report(&self, colored: bool) -> crate::Result { let mut buf = String::new(); - let theme = if colored { - GraphicalTheme::unicode() + + let h = if colored { + &COLORED_GRAPHICAL_REPORT_HANDLER } else { - GraphicalTheme::unicode_nocolor() + &NO_COLOR_GRAPHICAL_REPORT_HANDLER }; - let h = GraphicalReportHandler::new() - .with_theme(theme) - .with_context_lines(2) - .with_width(usize::MAX); + h.render_report(&mut buf, self.as_ref()).into_diagnostic()?; Ok(buf) } diff --git a/crates/rspack_error/src/graphical.rs b/crates/rspack_error/src/graphical.rs index b5bbc32f8959..2f1ee26b637d 100644 --- a/crates/rspack_error/src/graphical.rs +++ b/crates/rspack_error/src/graphical.rs @@ -7,12 +7,14 @@ use std::fmt::{self, Write}; use miette::{ - Diagnostic, GraphicalTheme, LabeledSpan, MietteError, ReportHandler, Severity, SourceCode, - SourceSpan, SpanContents, + highlighters::Highlighter, Diagnostic, GraphicalTheme, LabeledSpan, MietteError, ReportHandler, + Severity, SourceCode, SourceSpan, SpanContents, }; -use owo_colors::{OwoColorize, Style}; +use owo_colors::{OwoColorize, Style, StyledList}; use unicode_width::UnicodeWidthChar; +use crate::highlighters::MietteHighlighter; + /** A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a quasi-graphical way, using terminal colors, unicode drawing characters, and @@ -35,6 +37,7 @@ pub struct GraphicalReportHandler { pub(crate) context_lines: usize, pub(crate) tab_width: usize, pub(crate) with_cause_chain: bool, + pub(crate) highlighter: MietteHighlighter, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -56,6 +59,7 @@ impl GraphicalReportHandler { context_lines: 1, tab_width: 4, with_cause_chain: true, + highlighter: MietteHighlighter::default(), } } @@ -69,6 +73,7 @@ impl GraphicalReportHandler { context_lines: 1, tab_width: 4, with_cause_chain: true, + highlighter: MietteHighlighter::default(), } } @@ -138,6 +143,23 @@ impl GraphicalReportHandler { self.context_lines = lines; self } + + /// Enable syntax highlighting for source code snippets, using the given + /// [`Highlighter`]. See the [crate::highlighters] crate for more details. + pub fn with_syntax_highlighting( + mut self, + highlighter: impl Highlighter + Send + Sync + 'static, + ) -> Self { + self.highlighter = MietteHighlighter::from(highlighter); + self + } + + /// Disable syntax highlighting. This uses the + /// [`crate::highlighters::BlankHighlighter`] as a no-op highlighter. + pub fn without_syntax_highlighting(mut self) -> Self { + self.highlighter = MietteHighlighter::nocolor(); + self + } } impl Default for GraphicalReportHandler { @@ -398,6 +420,8 @@ impl GraphicalReportHandler { .map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st)) .collect::>(); + let mut highlighter_state = self.highlighter.start_highlighter_state(&*contents); + // The max number of gutter-lines that will be active at any given // point. We need this to figure out indentation, so we do one loop // over the lines to see what the damage is gonna be. @@ -461,7 +485,8 @@ impl GraphicalReportHandler { self.render_line_gutter(f, max_gutter, line, &labels)?; // And _now_ we can print out the line text itself! - self.render_line_text(f, &line.text)?; + let styled_text = StyledList::from(highlighter_state.highlight_line(&line.text)).to_string(); + self.render_line_text(f, &styled_text)?; // Next, we write all the highlights that apply to this particular line. let (single_line, multi_line): (Vec<_>, Vec<_>) = labels @@ -620,13 +645,26 @@ impl GraphicalReportHandler { /// Returns an iterator over the visual width of each character in a line. fn line_visual_char_width<'a>(&self, text: &'a str) -> impl Iterator + 'a { let mut column = 0; + let mut escaped = false; let tab_width = self.tab_width; text.chars().map(move |c| { - let width = if c == '\t' { + let width = match (escaped, c) { // Round up to the next multiple of tab_width - tab_width - column % tab_width - } else { - c.width().unwrap_or(0) + (false, '\t') => tab_width - column % tab_width, + // start of ANSI escape + (false, '\x1b') => { + escaped = true; + 0 + } + // use Unicode width for all other characters + (false, c) => c.width().unwrap_or(0), + // end of ANSI escape + (true, 'm') => { + escaped = false; + 0 + } + // characters are zero width within escape sequence + (true, _) => 0, }; column += width; width diff --git a/crates/rspack_error/src/highlighters/mod.rs b/crates/rspack_error/src/highlighters/mod.rs new file mode 100644 index 000000000000..8b9e3407ac6a --- /dev/null +++ b/crates/rspack_error/src/highlighters/mod.rs @@ -0,0 +1,76 @@ +//! This module provides a trait for creating custom syntax highlighters that +//! highlight [`Diagnostic`](crate::Diagnostic) source code with ANSI escape +//! sequences when rendering with the [`GraphicalReportHighlighter`](crate::graphical::GraphicalReportHandler). +//! +//! It also provides built-in highlighter implementations that you can use out of the box. +//! By default, there are no syntax highlighters exported by miette +//! (except for the no-op [`BlankHighlighter`]). +//! To enable support for specific highlighters, you should enable their associated feature flag. +//! +//! Currently supported syntax highlighters and their feature flags: +//! * `syntect-highlighter` - Enables [`syntect`](https://docs.rs/syntect/latest/syntect/) syntax highlighting support via the [`SyntectHighlighter`] + +/// THIS FILE IS ORIGINALLY FROM THE MIETTE PROJECT: +/// https://github.com/zkat/miette/blob/907857058dc255caeae456e87146c629ce69cf5c/src/highlighters/mod.rs +use std::{ops::Deref, sync::Arc}; + +#[cfg(not(target_family = "wasm"))] +use miette::highlighters::SyntectHighlighter; +use miette::highlighters::{BlankHighlighter, Highlighter}; + +/// Arcified trait object for Highlighter. Used internally by [`crate::graphical::GraphicalReportHandler`] +/// +/// Wrapping the trait object in this way allows us to implement `Debug` and `Clone`. +#[derive(Clone)] +#[repr(transparent)] +pub(crate) struct MietteHighlighter(Arc); + +impl MietteHighlighter { + pub(crate) fn nocolor() -> Self { + Self::from(BlankHighlighter) + } + + #[cfg(not(target_family = "wasm"))] + pub(crate) fn syntect_truecolor() -> Self { + Self::from(SyntectHighlighter::default()) + } +} + +impl Default for MietteHighlighter { + #[cfg(not(target_family = "wasm"))] + fn default() -> Self { + use std::io::IsTerminal; + match std::env::var("NO_COLOR") { + _ if !std::io::stdout().is_terminal() || !std::io::stderr().is_terminal() => { + //TODO: should use ANSI styling instead of 24-bit truecolor here + MietteHighlighter::syntect_truecolor() + } + Ok(string) if string != "0" => MietteHighlighter::nocolor(), + _ => MietteHighlighter::syntect_truecolor(), + } + } + + #[cfg(target_family = "wasm")] + fn default() -> Self { + MietteHighlighter::nocolor() + } +} + +impl From for MietteHighlighter { + fn from(value: T) -> Self { + Self(Arc::new(value)) + } +} + +impl std::fmt::Debug for MietteHighlighter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "MietteHighlighter(...)") + } +} + +impl Deref for MietteHighlighter { + type Target = dyn Highlighter + Send + Sync; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} diff --git a/crates/rspack_error/src/lib.rs b/crates/rspack_error/src/lib.rs index ac857d0829a5..d3e7a97e4971 100644 --- a/crates/rspack_error/src/lib.rs +++ b/crates/rspack_error/src/lib.rs @@ -7,6 +7,7 @@ mod diagnostic; mod error; mod ext; pub(crate) mod graphical; +pub(crate) mod highlighters; pub(crate) mod miette_helpers; pub use catch_unwind::*; pub use diagnostic::*; diff --git a/packages/rspack-test-tools/src/processor/diagnostic.ts b/packages/rspack-test-tools/src/processor/diagnostic.ts index ac292fc0d6c1..a75fec75f7d8 100644 --- a/packages/rspack-test-tools/src/processor/diagnostic.ts +++ b/packages/rspack-test-tools/src/processor/diagnostic.ts @@ -45,6 +45,7 @@ export class DiagnosticProcessor< const statsJson = stats.toJson({ all: false, + colors: false, errors: true, warnings: true }); diff --git a/packages/rspack-test-tools/tests/configCases/context/issue-10889/index.js b/packages/rspack-test-tools/tests/configCases/context/issue-10889/index.js index 30ee1cf4e66c..5895c9a80f6d 100644 --- a/packages/rspack-test-tools/tests/configCases/context/issue-10889/index.js +++ b/packages/rspack-test-tools/tests/configCases/context/issue-10889/index.js @@ -10,4 +10,3 @@ it('should parse invalid regex syntax', () => { console.log('regex:', a, b, c, d, f, h) }) - diff --git a/xtask/benchmark/benches/groups/bundle/util.rs b/xtask/benchmark/benches/groups/bundle/util.rs index d6e06ef99b10..adcce21181aa 100644 --- a/xtask/benchmark/benches/groups/bundle/util.rs +++ b/xtask/benchmark/benches/groups/bundle/util.rs @@ -3,7 +3,7 @@ use std::{path::PathBuf, sync::Arc}; use rspack::builder::{Builder, CompilerBuilder}; use rspack_core::{ Compiler, Experiments, Mode, ModuleOptions, ModuleRule, ModuleRuleEffect, ModuleRuleUse, - ModuleRuleUseLoader, Resolve, RuleSetCondition, + ModuleRuleUseLoader, Resolve, RuleSetCondition, StatsOptions, }; use rspack_fs::{MemoryFileSystem, NativeFileSystem}; use rspack_regex::RspackRegex; @@ -59,6 +59,7 @@ pub fn basic_compiler_builder(options: BuilderOptions) -> CompilerBuilder { extensions: Some(vec!["...".to_string(), ".jsx".to_string()]), ..Default::default() }) + .stats(StatsOptions { colors: false }) .experiments(Experiments::builder().css(true)) .input_filesystem(Arc::new(NativeFileSystem::new(false))) .output_filesystem(Arc::new(MemoryFileSystem::default()))