Skip to content

Commit 1ded3fc

Browse files
Merge branch 'main' into issue-8739
2 parents 3040b5d + 529f54b commit 1ded3fc

File tree

7 files changed

+138
-38
lines changed

7 files changed

+138
-38
lines changed

crates/rolldown_binding/src/types/binding_magic_string.rs

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,64 @@ use rolldown_utils::base64::to_standard_base64;
99
use serde::Serialize;
1010
use string_wizard::{MagicString, MagicStringOptions, SourceMapOptions};
1111

12+
/// Internal representation preserving the original JS format (flat `[start, end]` vs nested
13+
/// `[[start, end], ...]`) so the getter returns the same shape the user passed in.
14+
#[derive(Clone)]
15+
enum IndentExclusionRanges {
16+
Flat(Vec<i64>),
17+
Nested(Vec<Vec<i64>>),
18+
}
19+
20+
impl IndentExclusionRanges {
21+
fn from_either(either: Either<Vec<Vec<i64>>, Vec<i64>>) -> Self {
22+
match either {
23+
Either::A(nested) => Self::Nested(nested),
24+
Either::B(flat) => Self::Flat(flat),
25+
}
26+
}
27+
28+
fn to_either(&self) -> Either<Vec<Vec<i64>>, Vec<i64>> {
29+
match self {
30+
Self::Flat(v) => Either::B(v.clone()),
31+
Self::Nested(v) => Either::A(v.clone()),
32+
}
33+
}
34+
}
35+
36+
/// Normalizes an `Either<Vec<Vec<i64>>, Vec<i64>>` (nested or flat exclusion ranges from JS)
37+
/// into `Vec<(u32, u32)>` byte-offset pairs suitable for the Rust indent implementation.
38+
/// The `offset` is applied to each index before UTF-16→byte conversion, matching the
39+
/// behavior of every other position-based API in this binding.
40+
fn normalize_exclude_ranges(
41+
ranges: &Either<Vec<Vec<i64>>, Vec<i64>>,
42+
mapper: &Utf16ToByteMapper,
43+
offset: i64,
44+
) -> Vec<(u32, u32)> {
45+
let pairs: Vec<(i64, i64)> = match ranges {
46+
Either::B(flat) => {
47+
if flat.len() >= 2 {
48+
vec![(flat[0], flat[1])]
49+
} else {
50+
vec![]
51+
}
52+
}
53+
Either::A(nested) => {
54+
nested.iter().filter_map(|r| if r.len() >= 2 { Some((r[0], r[1])) } else { None }).collect()
55+
}
56+
};
57+
58+
pairs
59+
.into_iter()
60+
.filter_map(|(s, e)| {
61+
let s_with_offset = u32::try_from(s + offset).ok()?;
62+
let e_with_offset = u32::try_from(e + offset).ok()?;
63+
let start = mapper.utf16_to_byte(s_with_offset)?;
64+
let end = mapper.utf16_to_byte(e_with_offset)?;
65+
Some((start, end))
66+
})
67+
.collect()
68+
}
69+
1270
/// Serializable source map matching the SourceMap V3 specification.
1371
#[derive(Serialize)]
1472
#[serde(rename_all = "camelCase")]
@@ -119,6 +177,13 @@ impl Utf16ToByteMapper {
119177
pub struct BindingMagicStringOptions {
120178
pub filename: Option<String>,
121179
pub offset: Option<i64>,
180+
pub indent_exclusion_ranges: Option<Either<Vec<Vec<i64>>, Vec<i64>>>,
181+
}
182+
183+
#[napi(object)]
184+
#[derive(Default)]
185+
pub struct BindingIndentOptions {
186+
pub exclude: Option<Either<Vec<Vec<i64>>, Vec<i64>>>,
122187
}
123188

124189
#[napi(object)]
@@ -280,6 +345,7 @@ pub struct BindingMagicString<'a> {
280345
pub(crate) inner: MagicString<'a>,
281346
utf16_to_byte_mapper: Utf16ToByteMapper,
282347
pub(crate) offset: i64,
348+
indent_exclusion_ranges: Option<IndentExclusionRanges>,
283349
}
284350

285351
#[napi]
@@ -289,11 +355,14 @@ impl BindingMagicString<'_> {
289355
let utf16_to_byte_mapper = Utf16ToByteMapper::new(&source);
290356
let opts = options.unwrap_or_default();
291357
let offset = opts.offset.unwrap_or(0);
358+
let indent_exclusion_ranges =
359+
opts.indent_exclusion_ranges.map(IndentExclusionRanges::from_either);
292360
let magic_string_options = MagicStringOptions { filename: opts.filename };
293361
Self {
294362
inner: MagicString::with_options(source, magic_string_options),
295363
utf16_to_byte_mapper,
296364
offset,
365+
indent_exclusion_ranges,
297366
}
298367
}
299368

@@ -307,6 +376,11 @@ impl BindingMagicString<'_> {
307376
self.inner.filename()
308377
}
309378

379+
#[napi(getter)]
380+
pub fn indent_exclusion_ranges(&self) -> Option<Either<Vec<Vec<i64>>, Vec<i64>>> {
381+
self.indent_exclusion_ranges.as_ref().map(IndentExclusionRanges::to_either)
382+
}
383+
310384
#[napi(getter)]
311385
pub fn get_offset(&self) -> i64 {
312386
self.offset
@@ -558,14 +632,26 @@ impl BindingMagicString<'_> {
558632
}
559633

560634
#[napi]
561-
pub fn indent<'s>(&'s mut self, this: This<'s>, indentor: Option<String>) -> This<'s> {
562-
if let Some(indentor) = indentor {
563-
self
564-
.inner
565-
.indent_with(string_wizard::IndentOptions { indentor: Some(&indentor), exclude: &[] });
635+
pub fn indent<'s>(
636+
&'s mut self,
637+
this: This<'s>,
638+
indentor: Option<String>,
639+
options: Option<BindingIndentOptions>,
640+
) -> This<'s> {
641+
// Per-call exclude takes priority; fall back to constructor's indentExclusionRanges.
642+
let explicit_exclude = options.and_then(|opts| opts.exclude);
643+
let exclude_ranges = if let Some(ref e) = explicit_exclude {
644+
normalize_exclude_ranges(e, &self.utf16_to_byte_mapper, self.offset)
645+
} else if let Some(ref stored) = self.indent_exclusion_ranges {
646+
normalize_exclude_ranges(&stored.to_either(), &self.utf16_to_byte_mapper, self.offset)
566647
} else {
567-
self.inner.indent();
568-
}
648+
vec![]
649+
};
650+
651+
self.inner.indent_with(string_wizard::IndentOptions {
652+
indentor: indentor.as_deref(),
653+
exclude: &exclude_ranges,
654+
});
569655
this
570656
}
571657

@@ -614,6 +700,7 @@ impl BindingMagicString<'_> {
614700
inner: self.inner.clone(),
615701
utf16_to_byte_mapper: self.utf16_to_byte_mapper.clone(),
616702
offset: self.offset,
703+
indent_exclusion_ranges: self.indent_exclusion_ranges.clone(),
617704
}
618705
}
619706

@@ -644,6 +731,7 @@ impl BindingMagicString<'_> {
644731
inner: self.inner.snip(start_byte, end_byte).map_err(napi::Error::from_reason)?,
645732
utf16_to_byte_mapper: self.utf16_to_byte_mapper.clone(),
646733
offset: self.offset,
734+
indent_exclusion_ranges: self.indent_exclusion_ranges.clone(),
647735
})
648736
}
649737

docs/.vitepress/theme/Hero.vue

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,23 @@
2020
href="https://github.com/rolldown/rolldown"
2121
target="_blank"
2222
rel="noopener noreferrer"
23-
class="button inline-block w-fit"
23+
class="button inline-flex items-center gap-2 w-fit"
2424
>
25-
View on GitHub
25+
<span>View on GitHub</span>
26+
<svg class="size-3" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
27+
<path
28+
d="M3.18228 2.81797L9.54624 2.81797L9.54624 9.18193"
29+
stroke="currentColor"
30+
stroke-width="1.35"
31+
stroke-linejoin="round"
32+
/>
33+
<path
34+
d="M9.5459 2.81799L3.18194 9.18195"
35+
stroke="currentColor"
36+
stroke-width="1.35"
37+
stroke-linejoin="round"
38+
/>
39+
</svg>
2640
</a>
2741
</div>
2842
</div>

docs/guide/notable-features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Note it behaves differently from [`@rollup/plugin-replace`](https://github.com/r
6363

6464
- Configurable via the [`transform.inject`](/reference/InputOptions.transform#inject) option.
6565

66-
This feature provides a way to shim global variables with a specific value exported from a module. This feature is equivalent of [esbuild's `inject` option](https://esbuild.github.io/api/#inject) and [`@rollup/plugin-inject`](https://github.com/rollup/plugins/tree/master/packages/inject).
66+
This feature provides a way to shim global variables with a specific value exported from a module. This feature is equivalent of [`@rollup/plugin-inject`](https://github.com/rollup/plugins/tree/master/packages/inject) and conceptually similar to [esbuild's `inject` option](https://esbuild.github.io/api/#inject).
6767

6868
## CSS bundling
6969

packages/rolldown/src/binding.d.cts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1480,6 +1480,7 @@ export declare class BindingMagicString {
14801480
constructor(source: string, options?: BindingMagicStringOptions | undefined | null)
14811481
get original(): string
14821482
get filename(): string | null
1483+
get indentExclusionRanges(): Array<Array<number>> | Array<number> | null
14831484
get offset(): number
14841485
set offset(offset: number)
14851486
replace(from: string, to: string): this
@@ -1504,7 +1505,7 @@ export declare class BindingMagicString {
15041505
* Returns `this` for method chaining.
15051506
*/
15061507
move(start: number, end: number, index: number): this
1507-
indent(indentor?: string | undefined | null): this
1508+
indent(indentor?: string | undefined | null, options?: BindingIndentOptions | undefined | null): this
15081509
/** Trims whitespace or specified characters from the start and end. */
15091510
trim(charType?: string | undefined | null): this
15101511
/** Trims whitespace or specified characters from the start. */
@@ -2187,6 +2188,10 @@ export interface BindingHookTransformOutput {
21872188
moduleType?: string
21882189
}
21892190

2191+
export interface BindingIndentOptions {
2192+
exclude?: Array<Array<number>> | Array<number>
2193+
}
2194+
21902195
export interface BindingInjectImportNamed {
21912196
tagNamed: true
21922197
imported: string
@@ -2292,6 +2297,7 @@ export interface BindingLogLocation {
22922297
export interface BindingMagicStringOptions {
22932298
filename?: string
22942299
offset?: number
2300+
indentExclusionRanges?: Array<Array<number>> | Array<number>
22952301
}
22962302

22972303
export type BindingMakeAbsoluteExternalsRelative =

packages/rolldown/tests/magic-string/MagicString.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ describe('MagicString', () => {
153153
assert.equal(c.filename, 'foo.js');
154154
});
155155

156-
it.skip('should clone indentExclusionRanges', () => {
156+
it('should clone indentExclusionRanges', () => {
157157
const array = [3, 6];
158158
const source = new MagicString('abcdefghijkl', {
159159
filename: 'foo.js',
@@ -166,7 +166,7 @@ describe('MagicString', () => {
166166
assert.deepEqual(source.indentExclusionRanges, clone.indentExclusionRanges);
167167
});
168168

169-
it.skip('should clone complex indentExclusionRanges', () => {
169+
it('should clone complex indentExclusionRanges', () => {
170170
const array = [
171171
[3, 6],
172172
[7, 9],
@@ -679,7 +679,7 @@ describe('MagicString', () => {
679679
assert.equal(s.toString(), 'abc\ndef\nghi\njkl');
680680
});
681681

682-
it.skip('should prevent excluded characters from being indented', () => {
682+
it('should prevent excluded characters from being indented', () => {
683683
const s = new MagicString('abc\ndef\nghi\njkl');
684684

685685
s.indent(' ', { exclude: [7, 15] });
@@ -1317,7 +1317,7 @@ describe('MagicString', () => {
13171317
assert.equal(s.toString(), 'a[]c;');
13181318
});
13191319

1320-
it.skip('should provide a useful error when illegal removals are attempted', () => {
1320+
it('should provide a useful error when illegal removals are attempted', () => {
13211321
const s = new MagicString('abcdefghijkl');
13221322

13231323
s.overwrite(5, 7, 'XX');
@@ -1419,7 +1419,7 @@ describe('MagicString', () => {
14191419
assert.equal(s.toString(), 'bc');
14201420
});
14211421

1422-
it.skip('should reset modified ranges', () => {
1422+
it('should reset modified ranges', () => {
14231423
const s = new MagicString('abcdefghi');
14241424

14251425
s.overwrite(3, 6, 'DEF');
@@ -1451,7 +1451,7 @@ describe('MagicString', () => {
14511451
assert.equal(s.toString(), '(a.c);');
14521452
});
14531453

1454-
it.skip('should provide a useful error when illegal removals are attempted', () => {
1454+
it('should provide a useful error when illegal removals are attempted', () => {
14551455
const s = new MagicString('abcdefghijkl');
14561456

14571457
s.remove(4, 8);

packages/rolldown/tests/magic-string/download-tests.mjs

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* 3. Skips tests that use unsupported features
1212
*
1313
* BindingMagicString API (supported methods):
14-
* - constructor(source: string, options?: { filename?, offset? })
14+
* - constructor(source: string, options?: { filename?, offset?, indentExclusionRanges? })
1515
* - offset: number (getter/setter — shifts all position-based operations)
1616
* - replace(from: string, to: string): this
1717
* - replaceAll(from: string, to: string): this
@@ -30,7 +30,7 @@
3030
* - update(start: number, end: number, content: string): this
3131
* - relocate(start: number, end: number, to: number): this
3232
* - move(start: number, end: number, index: number): this (alias for relocate)
33-
* - indent(indentor?: string | undefined | null): this
33+
* - indent(indentor?: string | undefined | null, options?: { exclude? }): this
3434
* - slice(start?: number, end?: number): string
3535
* - insert(index: number, content: string): throws Error (deprecated)
3636
* - clone(): BindingMagicString
@@ -42,12 +42,10 @@
4242
* - generateDecodedMap(options?): BindingDecodedMap (returns object with decoded mappings array)
4343
*
4444
* NOT supported (will be skipped):
45-
* - constructor options (ignoreList, indentExclusionRanges) — filename and offset ARE supported
46-
* - reset tests (most require splitting inside edited chunks)
45+
* - constructor options: ignoreList — filename, offset, and indentExclusionRanges ARE supported
4746
* - addSourcemapLocation (not in string_wizard)
48-
* - storeName option in overwrite (not in string_wizard)
49-
* - x_google_ignoreList / ignoreList (not in string_wizard)
50-
* - original property (getter — not yet implemented)
47+
* - storeName option in overwrite/update (not exposed in binding)
48+
* - x_google_ignoreList / ignoreList in generateMap output (not in string_wizard)
5149
* - replace/replaceAll with regex or function replacer
5250
*/
5351

@@ -87,7 +85,7 @@ const SKIP_TESTS = [
8785
'should throw', // error handling differs
8886
// options-specific skips
8987
'stores ignore-list hint', // ignoreList option not supported
90-
'indentExclusionRanges', // not supported
88+
// Note: 'indentExclusionRanges' is now supported (constructor option + getter + clone)
9189
'sourcemapLocations', // not supported
9290
'should return cloned content', // clone-related
9391
'should noop', // edge cases that may differ
@@ -108,7 +106,7 @@ const SKIP_TESTS = [
108106

109107
'should replace then remove', // causes split chunk panic
110108
'preserves intended order', // complex append/prepend ordering with slice
111-
'excluded characters', // indent exclude option not supported
109+
// Note: 'excluded characters' (indent exclude option) is now supported
112110
// remove-specific skips
113111
'should remove everything', // edge case
114112
'should adjust other removals', // complex removal interaction
@@ -126,14 +124,13 @@ const SKIP_TESTS = [
126124
// The reset version passes, so we handle this with a special transformation below
127125
'should not remove content inserted', // complex interaction
128126
'should remove interior inserts', // causes panic
129-
'should provide a useful error', // expects throw but gets panic
127+
// Note: 'should provide a useful error' now works — errors are properly thrown, not panicked
130128
// slice-specific skips
131129
'should return the generated content between the specified original characters', // nested overwrites + slice
132130
'supports characters moved', // complex move + slice interaction
133131
// clone-specific skips (tests that use unsupported constructor options)
134132
// Note: 'should clone filename info' now works since filename is supported
135-
'should clone indentExclusionRanges', // uses indentExclusionRanges constructor option
136-
'should clone complex indentExclusionRanges', // uses indentExclusionRanges constructor option
133+
// Note: 'should clone indentExclusionRanges' now works since indentExclusionRanges is supported
137134
'should clone sourcemapLocations', // uses sourcemapLocations
138135
// hasChanged tests that use clone
139136
'should not report change if content is identical', // uses clone
@@ -180,12 +177,12 @@ function transformTestFile(content, filename) {
180177
// Replace imports
181178
transformed = transformed.replace(
182179
/import MagicString from ['"]\.\/utils\/IntegrityCheckingMagicString['"];?/g,
183-
"import { BindingMagicString as MagicString } from 'rolldown';",
180+
"import { RolldownMagicString as MagicString } from 'rolldown';",
184181
);
185182

186183
transformed = transformed.replace(
187184
/import MagicString from ['"]\.\.\/src\/MagicString['"];?/g,
188-
"import { BindingMagicString as MagicString } from 'rolldown';",
185+
"import { RolldownMagicString as MagicString } from 'rolldown';",
189186
);
190187

191188
// Handle Bundle import - Bundle is not supported, so we import MagicString and skip all Bundle tests
@@ -264,12 +261,7 @@ function transformTestFile(content, filename) {
264261
"$1\n$2it.skip('removes across moved content'",
265262
);
266263

267-
// Special case: skip "should reset modified ranges" but not "should reset modified ranges, redux"
268-
// The "redux" version passes, but the first one uses overwrite+remove+reset which fails
269-
transformed = transformed.replace(
270-
/it\('should reset modified ranges', /g,
271-
"it.skip('should reset modified ranges', ",
272-
);
264+
// Note: "should reset modified ranges" now passes (overwrite+remove+reset works correctly)
273265

274266
return transformed;
275267
}

test262

Submodule test262 updated 111 files

0 commit comments

Comments
 (0)