Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
725bef9
Be defensive rendering code frames
lukesandberg Oct 30, 2025
1563ca6
Add next-code-frame crate with core rendering logic
lukesandberg Oct 31, 2025
538cc95
Fix multiline error markers and add column validation tests
lukesandberg Oct 31, 2025
8f4e827
Add spanning markers for multiline errors
lukesandberg Oct 31, 2025
67a350f
Simplify marker rendering logic with upfront normalization
lukesandberg Oct 31, 2025
9ec6a8c
Simplify marker column calculation with saturating arithmetic
lukesandberg Oct 31, 2025
d7a6dbe
Optimize repeated character output to avoid allocations
lukesandberg Oct 31, 2025
a244a43
Extract repeat_char_into helper for efficient character repetition
lukesandberg Oct 31, 2025
1204575
Apply clippy suggestions for idiomatic Rust
lukesandberg Oct 31, 2025
0c5a4a5
Fix some more clippy issues
lukesandberg Oct 31, 2025
427ae1c
Extract helper functions for line truncation and marker calculation
lukesandberg Oct 31, 2025
7dcd5e5
Clarify end_column semantics with detailed comments
lukesandberg Oct 31, 2025
6ece5d4
Convert code frame API to nested Location structure
lukesandberg Oct 31, 2025
873d84b
Migrate tests to use insta snapshot testing
lukesandberg Oct 31, 2025
fa68205
use inline snapshots
lukesandberg Oct 31, 2025
d630d5b
Implement Phase 4: Syntax highlighting architecture with OXC
lukesandberg Oct 31, 2025
d45da75
Phase 4: Fix swc_ecma_lexer import errors
lukesandberg Oct 31, 2025
e861779
Phase 4: Add syntax highlighting tests and fix BytePos offset
lukesandberg Oct 31, 2025
0f9bf70
Phase 4: Integrate syntax highlighting into code frame rendering
lukesandberg Oct 31, 2025
b4c11c5
Add syntax highlighting demo example
lukesandberg Oct 31, 2025
0838c8e
Add comments and punctuation highlighting matching Babel
lukesandberg Oct 31, 2025
597c3d7
Add comprehensive syntax highlighting demo
lukesandberg Oct 31, 2025
125dbaf
Move strip_ansi_codes to test module and enable highlighting in all t…
lukesandberg Oct 31, 2025
2376fe2
Refactor highlighting to use TokenAndSpan.had_line_break and simplify…
lukesandberg Oct 31, 2025
6d919d6
Optimize highlighting: use had_line_break and line_bounds API
lukesandberg Oct 31, 2025
7006eb0
Optimize highlight.rs to only produce markers for visible lines
lukesandberg Nov 1, 2025
babdb85
Add NAPI bindings for next-code-frame
lukesandberg Nov 1, 2025
814b58d
Fix the napi bindings and support wasm as well.
lukesandberg Nov 4, 2025
8e9cbdd
more async hacks
lukesandberg Nov 4, 2025
e8d5fc3
Make code frame rendering synchronous
lukesandberg Nov 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"crates/next-api",
"crates/next-build-test",
"crates/next-build",
"crates/next-code-frame",
"crates/next-core",
"crates/next-custom-transforms",
"crates/next-taskless",
Expand Down Expand Up @@ -287,6 +288,7 @@ debug = true
# Workspace crates
next-api = { path = "crates/next-api" }
next-build = { path = "crates/next-build" }
next-code-frame = { path = "crates/next-code-frame" }
next-core = { path = "crates/next-core" }
next-custom-transforms = { path = "crates/next-custom-transforms" }
next-taskless = { path = "crates/next-taskless" }
Expand Down
1 change: 1 addition & 0 deletions crates/napi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ futures-util = { workspace = true }
owo-colors = { workspace = true }
napi = { workspace = true }
napi-derive = "2"
next-code-frame = { workspace = true }
next-custom-transforms = { workspace = true }
next-taskless = { workspace = true }
rand = { workspace = true }
Expand Down
89 changes: 89 additions & 0 deletions crates/napi/src/code_frame.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use napi::bindgen_prelude::*;
use next_code_frame::{CodeFrameLocation, CodeFrameOptions, Location, render_code_frame};

#[napi(object)]

Check failure on line 4 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / build / build

cannot find attribute `napi` in this scope

Check failure on line 4 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / build-native / build

cannot find attribute `napi` in this scope

Check failure on line 4 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / build-native-windows / build

cannot find attribute `napi` in this scope

Check failure on line 4 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / stable - x86_64-unknown-linux-gnu - node@16

cannot find attribute `napi` in this scope
pub struct NapiLocation {
pub line: u32,
pub column: Option<u32>,
}

impl From<NapiLocation> for Location {
fn from(loc: NapiLocation) -> Self {
Location {
line: loc.line as usize,
column: loc.column.unwrap_or(0) as usize,
}
}
}

#[napi(object)]

Check failure on line 19 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / build / build

cannot find attribute `napi` in this scope

Check failure on line 19 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / build-native / build

cannot find attribute `napi` in this scope

Check failure on line 19 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / build-native-windows / build

cannot find attribute `napi` in this scope

Check failure on line 19 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / stable - x86_64-unknown-linux-gnu - node@16

cannot find attribute `napi` in this scope
pub struct NapiCodeFrameLocation {
pub start: NapiLocation,
pub end: Option<NapiLocation>,
}

impl From<NapiCodeFrameLocation> for CodeFrameLocation {
fn from(loc: NapiCodeFrameLocation) -> Self {
CodeFrameLocation {
start: loc.start.into(),
end: loc.end.map(Into::into),
}
}
}

#[napi(object)]

Check failure on line 34 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / build / build

cannot find attribute `napi` in this scope

Check failure on line 34 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / build-native / build

cannot find attribute `napi` in this scope

Check failure on line 34 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / build-native-windows / build

cannot find attribute `napi` in this scope

Check failure on line 34 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / stable - x86_64-unknown-linux-gnu - node@16

cannot find attribute `napi` in this scope
#[derive(Default)]
pub struct NapiCodeFrameOptions {
/// Number of lines to show above the error (default: 2)
pub lines_above: Option<u32>,
/// Number of lines to show below the error (default: 3)
pub lines_below: Option<u32>,
/// Maximum width of the output (default: terminal width)
pub max_width: Option<u32>,
/// Whether to use ANSI colors (default: true)
pub force_color: Option<bool>,
/// Whether to highlight code syntax (default: true)
pub highlight_code: Option<bool>,
/// Optional message to display with the code frame
pub message: Option<String>,
}

impl From<NapiCodeFrameOptions> for CodeFrameOptions {
fn from(opts: NapiCodeFrameOptions) -> Self {
CodeFrameOptions {
lines_above: opts.lines_above.unwrap_or(2) as usize,
lines_below: opts.lines_below.unwrap_or(3) as usize,
max_width: opts.max_width.map(|w| w as usize),
use_colors: opts.force_color.unwrap_or(true),
highlight_code: opts.highlight_code.unwrap_or(true),
message: opts.message,
}
}
}

/// Renders a code frame showing the location of an error in source code
///
/// This is a Rust implementation that replaces Babel's code-frame for better:
/// - Performance on large files
/// - Handling of long lines
/// - Memory efficiency
///
/// # Arguments
/// * `source` - The source code to render
/// * `location` - The location to highlight (line and column numbers are 1-indexed)
/// * `options` - Optional configuration
///
/// # Returns
/// The formatted code frame string, or empty string if the location is invalid
#[napi]

Check failure on line 78 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / build / build

cannot find attribute `napi` in this scope

Check failure on line 78 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / build-native / build

cannot find attribute `napi` in this scope

Check failure on line 78 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / build-native-windows / build

cannot find attribute `napi` in this scope

Check failure on line 78 in crates/napi/src/code_frame.rs

View workflow job for this annotation

GitHub Actions / stable - x86_64-unknown-linux-gnu - node@16

cannot find attribute `napi` in this scope
pub fn code_frame_columns(
source: String,
location: NapiCodeFrameLocation,
options: Option<NapiCodeFrameOptions>,
) -> Result<String> {
let code_frame_location: CodeFrameLocation = location.into();
let code_frame_options: CodeFrameOptions = options.unwrap_or_default().into();

render_code_frame(&source, &code_frame_location, &code_frame_options)
.map_err(|e| Error::from_reason(format!("Failed to render code frame: {}", e)))
}
1 change: 1 addition & 0 deletions crates/napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ use swc_core::{
common::{FilePathMapping, SourceMap},
};

pub mod code_frame;
#[cfg(not(target_arch = "wasm32"))]
pub mod css;
pub mod lockfile;
Expand Down
25 changes: 25 additions & 0 deletions crates/next-code-frame/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "next-code-frame"
version = "0.0.1"
description = "Fast, scalable code frame rendering for Next.js error reporting"
license = "MIT"
edition = "2024"

[lib]
bench = false

[lints]
workspace = true

[profile.dev.package]
insta.opt-level = 3
similar.opt-level = 3

[dependencies]
anyhow = { workspace = true }
swc_ecma_lexer = "24.0.0"
swc_common = "15.0.0"
serde = { workspace = true }

[dev-dependencies]
insta = { version = "1.43.1", features = ["yaml"] }
24 changes: 24 additions & 0 deletions crates/next-code-frame/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# next-code-frame

Fast, scalable code frame rendering for Next.js error reporting, written in Rust.

This crate provides functionality similar to `@babel/code-frame` but with several improvements:
- **Scalability**: Handles arbitrarily large files efficiently
- **Long line handling**: Gracefully scrolls long lines to keep error positions visible and avoid overwhelming the terminal with long lines
- **Syntax highlighting**: Uses SWC lexer for accurate JavaScript/TypeScript tokenization

## Design

Following the `next-taskless` pattern, this crate:
- Has no dependency on turbo-tasks, allowing use in webpack/rspack codepaths
- Is compilable to WASM for environments without native bindings
- Follows "sans-io" patterns - accepts file content as arguments rather than performing IO
- Modifying it to optionally accept file paths is reasonable future work

## Features

- Terminal width detection with sensible defaults
- Syntax highlighting for JS, TS, JSX, TSX
- Graceful degradation for non-JS files or parsing errors
- ANSI color support matching babel-code-frame aesthetics
- Support for single-line and multi-line error ranges
27 changes: 27 additions & 0 deletions crates/next-code-frame/examples/comments_punctuation_demo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use next_code_frame::{CodeFrameLocation, CodeFrameOptions, Location, render_code_frame};

fn main() {
let source = r#"// This is a comment
const x = 42; // inline comment
const obj = { foo: 'bar' };
/* Multi-line
comment */
const result = x > 10 ? 'yes' : 'no';"#;

let location = CodeFrameLocation {
start: Location { line: 2, column: 1 },
end: Some(Location {
line: 2,
column: 14, // Mark "const x = 42;"
}),
};

println!("=== With Syntax Highlighting (showing comments and punctuation) ===");
let options = CodeFrameOptions {
use_colors: true,
highlight_code: true,
..Default::default()
};
let result = render_code_frame(source, &location, &options).unwrap();
println!("{}", result);
}
53 changes: 53 additions & 0 deletions crates/next-code-frame/examples/complete_highlighting_demo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use next_code_frame::{CodeFrameLocation, CodeFrameOptions, Location, render_code_frame};

fn main() {
let source = r#"// Type definition
const greeting: string = "Hello";
const count = 42 + 10;
const regex = /test\d+/gi;

/* Calculate result with
ternary operator */
const result = count > 10 ? "yes" : "no";

// Object literal
const obj = {
foo: 'bar',
baz: true,
};

// Arrow function with JSX
const Component = () => <div>Hello</div>;"#;

let location = CodeFrameLocation {
start: Location { line: 8, column: 1 },
end: Some(Location {
line: 8,
column: 43,
}),
};

println!("╔═══════════════════════════════════════════════════════════╗");
println!("║ Complete Syntax Highlighting Demo (matching Babel) ║");
println!("╚═══════════════════════════════════════════════════════════╝\n");

let options = CodeFrameOptions {
use_colors: true,
highlight_code: true,
..Default::default()
};
let result = render_code_frame(source, &location, &options).unwrap();
println!("{}", result);

println!("\n╔═══════════════════════════════════════════════════════════╗");
println!("║ Color Key: ║");
println!("╠═══════════════════════════════════════════════════════════╣");
println!("║ \x1b[36mKeywords\x1b[0m (cyan): const, let, var, if, etc. ║");
println!("║ \x1b[33mIdentifiers\x1b[0m (yellow): variable and function names ║");
println!("║ \x1b[32mStrings\x1b[0m (green): \"...\", '...', template literals ║");
println!("║ \x1b[35mNumbers\x1b[0m (magenta): 42, 0x10, bigints ║");
println!("║ \x1b[33mPunctuation\x1b[0m (yellow): = ; , . : ? + - * / ║");
println!("║ \x1b[90mComments\x1b[0m (gray): // and /* */ ║");
println!("║ Brackets (default): ( ) [ ] {{ }} ║");
println!("╚═══════════════════════════════════════════════════════════╝");
}
39 changes: 39 additions & 0 deletions crates/next-code-frame/examples/highlighting_demo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use next_code_frame::{CodeFrameLocation, CodeFrameOptions, Location, render_code_frame};

fn main() {
let source = r#"const greeting = "Hello, world!";
const number = 42;
const regex = /test/g;
const obj = { foo: 'bar' };"#;

let location = CodeFrameLocation {
start: Location {
line: 2,
column: 16,
},
end: Some(Location {
line: 2,
column: 18, // Mark "42"
}),
};

// Without highlighting
println!("=== Without Highlighting ===");
let options = CodeFrameOptions {
use_colors: true,
highlight_code: false,
..Default::default()
};
let result = render_code_frame(source, &location, &options).unwrap();
println!("{}", result);

// With highlighting
println!("\n=== With Syntax Highlighting ===");
let options = CodeFrameOptions {
use_colors: true,
highlight_code: true,
..Default::default()
};
let result = render_code_frame(source, &location, &options).unwrap();
println!("{}", result);
}
Loading
Loading