Skip to content

fix: keep fragments after the newline fragment in MagicString::last_line#10023

Merged
shulaoda merged 3 commits into
mainfrom
06-30-fix_keep_fragments_after_the_newline_fragment_in_magicstring_last_line
Jun 30, 2026
Merged

fix: keep fragments after the newline fragment in MagicString::last_line#10023
shulaoda merged 3 commits into
mainfrom
06-30-fix_keep_fragments_after_the_newline_fragment_in_magicstring_last_line

Conversation

@shulaoda

@shulaoda shulaoda commented Jun 30, 2026

Copy link
Copy Markdown
Member

What this solves

MagicString::last_line (exposed to JS as lastLine()) returns the content after the final newline of the generated string. Each intro/outro is a sequence of fragments, and the function scanned a deque from the back and, on the first fragment (from the end) that contained a newline, returned that fragment's tail. It never included the fragments it had already visited in that reverse scan, which come after the newline-bearing fragment in output order, so they were silently dropped. The same mistake was in the global-outro, per-chunk-outro and per-chunk-intro scans.

For example, appending "a\nb" then "c" produces "a\nbc", but lastLine() returned "b" instead of "bc".

The fix

Scan fragments from the back, pushing each fragment's relevant slice onto a buffer and stopping at the first newline that completes the last line. Sections (outro, chunks last-to-first, then intro) are visited only until that newline is found, so earlier sections are never touched, and the result is built once with an exact-capacity allocation. This matches the reference magic-string, which searches the concatenated outro/intro as a whole.

Tests

The regression tests live on the JS side next to the other hand-written magic-string cases (packages/rolldown/tests/magic-string/rolldown-magic-string.test.ts), since last_line is only reached through the lastLine() binding. Two lastLine guards cover a newline in a non-final outro fragment and a non-final intro fragment; both fail on the base (bc / qrx are dropped) and pass with the fix.

@netlify

netlify Bot commented Jun 30, 2026

Copy link
Copy Markdown

Deploy Preview for rolldown-rs canceled.

Name Link
🔨 Latest commit d1de7c9
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/6a439694fa480500089c8737

@codspeed-hq

codspeed-hq Bot commented Jun 30, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 7 untouched benchmarks
⏩ 10 skipped benchmarks1


Comparing 06-30-fix_keep_fragments_after_the_newline_fragment_in_magicstring_last_line (d1de7c9) with main (8483bbb)

Open in CodSpeed

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@IWANABETHATGUY

Copy link
Copy Markdown
Member

Fix looks good — the reverse-scan + pieces buffer correctly keeps the fragments after the newline-bearing one, and it's a bit leaner than the old per-chunk String rebuilds. Two things on the tests, though:

1. joins_content_tail_with_trailing_outro_fragments doesn't actually guard the regression — it passes on the base too.

In that case the \n lives in the chunk content, not in a non-final intro/outro fragment:

let mut s = MagicString::new("a\nb");
s.append("c");
s.append("d");
assert_eq!(s.last_line(), "bcd");

The old code already handled this — its content branch did return content[after_nl..] + line_str, and line_str had accumulated "cd" from the trailing outro fragments, so it returned "bcd" on main. I confirmed by running the equivalent through lastLine() on main: it passes unfixed. So the description's "they fail on the old code" isn't true for this one — it's a redundant test. The other two (keeps_outro_fragments_after_a_newline_fragment / keeps_intro_fragments_after_a_newline_fragment) are the real guards. Suggest dropping it or swapping it for a case that genuinely exercises the bug.

2. Could we put these regression tests on the JS side instead?

MagicString::last_line is only ever reached through the lastLine() napi binding, and we already keep the hand-written magic-string regression tests in packages/rolldown/tests/magic-string/rolldown-magic-string.test.ts. Keeping these next to the rest of the custom cases there (rather than in the crate) matches where the API is actually consumed and where reviewers will look for them. Something like:

describe('lastLine - keeps fragments after the newline fragment (#10023)', () => {
  it('keeps outro fragments appended after a newline fragment', () => {
    const s = new MagicString('x');
    s.append('pre');
    s.append('a\nb');
    s.append('c');
    assert.strictEqual(s.toString(), 'xprea\nbc');
    assert.strictEqual(s.lastLine(), 'bc');
  });

  it('keeps intro fragments prepended after a newline fragment', () => {
    const s = new MagicString('x');
    s.prepend('r');
    s.prepend('p\nq');
    s.prepend('pre');
    assert.strictEqual(s.toString(), 'prep\nqrx');
    assert.strictEqual(s.lastLine(), 'qrx');
  });
});

@shulaoda shulaoda merged commit 4468f65 into main Jun 30, 2026
34 checks passed
@shulaoda shulaoda deleted the 06-30-fix_keep_fragments_after_the_newline_fragment_in_magicstring_last_line branch June 30, 2026 10:24
@rolldown-guard rolldown-guard Bot mentioned this pull request Jul 1, 2026
shulaoda added a commit that referenced this pull request Jul 1, 2026
## [1.1.4] - 2026-07-01

### 🚀 Features

- disable `experimental.lazyBarrel` by default (#10071) by @shulaoda

### 🐛 Bug Fixes

- dev: disable lazy barrel in dev mode (#10060) by @shulaoda
- generate: keep full JSON interface under preserveModules namespa… (#10056) by @IWANABETHATGUY
- check finalize_other_specifiers in its own Debug attribute (#10032) by @shulaoda
- serialize the KeepAssign unused minify option as "keep_assign" (#10031) by @shulaoda
- keep fragments after the newline fragment in MagicString::last_line (#10023) by @shulaoda
- generate: undeclared JSON named exports under preserveModules (#10020) (#10027) by @IWANABETHATGUY
- deconflict: rename CJS-wrapped locals that shadow chunk-root bindings (#9921) by @IWANABETHATGUY
- rolldown: keep entry facade when a shared chunk holds another entry's module (#9997) by @hyf0
- treeshake: also bail JSON default split when the object escapes (#9996) by @IWANABETHATGUY
- don't classify await in a strict-mode function as top-level await (#9987) by @shulaoda
- avoid spurious leading newline in addon hooks (banner/footer/intro/outro) (#9989) by @shulaoda
- handle JSON default mutation bailouts (#9972) by @TheAlexLichter
- plugin: make lazy hook metadata enumerable (#9991) by @TheAlexLichter
- dev: make init errors in lazy-compiled modules catchable (#9981) by @h-a-n-a
- treeshake: keep computed-key side effects on namespace member access (#9986) by @shulaoda
- binding: validate replace plugin delimiters length instead of panicking (#9984) by @shulaoda
- reconstruct nested rest patterns in into_expression (#9980) by @IWANABETHATGUY
- reconstruct rest patterns as spread in into_expression (#9976) by @shulaoda
- preserve export keyword on multi-declarator exports under keepNames (#9974) by @shulaoda
- deterministically keep the shortest name for deduplicated assets (#9948) by @x1024
- treeshake: apply @__NO_SIDE_EFFECTS__ to cross-chunk namespace calls (#9960) by @IWANABETHATGUY

### 🚜 Refactor

- drop redundant program scope enter/leave in finalizer (#10049) by @shulaoda
- deconflict: extract collect_chunk_scope_captured_names (#10006) by @IWANABETHATGUY
- unify pre-scan multi-declarator split into one decision site (#9982) by @IWANABETHATGUY
- common: return bool from SymbolRef::is_not_reassigned (#9962) by @IWANABETHATGUY

### 📚 Documentation

- rolldown: remove outdated comment for removing parenthesized expression (#10062) by @Dunqing
- use GitHub-flavored alert for Etiquette note in contribution guide (#10012) by @IWANABETHATGUY
- replace: explain the delimiters left and right boundaries (#9985) by @shulaoda
- ast-mutation: remove stale Address Use section after pre-scan refactor (#9983) by @IWANABETHATGUY
- remove fathom (#9968) by @mdong1909
- contribution-guide: code-format main branch references (#9966) by @IWANABETHATGUY
- contribution-guide: fix stale REPL note and tidy wording (#9957) by @hyf0
- contribution-guide: clarify when to discuss before opening a PR (#9955) by @hyf0

### ⚡ Performance

- disable preserve_parens across all parse paths (#10057) by @Dunqing
- common: inline declared_symbols with SmallVec (#9920) by @IWANABETHATGUY
- common: pack TaggedSymbolRef into 8 bytes (#9919) by @IWANABETHATGUY
- sourcemap: skip newline scan on the no-sourcemap join fast path (#9936) by @Boshen

### 🧪 Testing

- dev: error in lazy module should be catchable (#9975) by @sapphi-red
- dev: reject unknown lazy compile modules (#9969) by @sapphi-red

### ⚙️ Miscellaneous Tasks

- deps: update actions/cache action to v6 (#10001) by @renovate[bot]
- trigger vite ecosystem-ci from PR comments (#10058) by @shulaoda
- deps: update napi to v3.10.0 (#10063) by @renovate[bot]
- remove unused From impl for RolldownLabelSpan (#10055) by @shulaoda
- remove dead Diagnostic::with_kind method (#10054) by @shulaoda
- remove unused StatementExt methods (#10053) by @shulaoda
- remove unused ExpressionExt methods (#10052) by @shulaoda
- remove commented-out re_export_all_names field (#10051) by @shulaoda
- deps: update pnpm to v11.9.0 (#10047) by @renovate[bot]
- remove the unused BindingGenerateHmrPatchReturn napi type (#10034) by @shulaoda
- remove the dead inline_entry_chunk_wrapping scaffolding (#10037) by @shulaoda
- deps: bump oxc_resolver to 11.22.0 (#10045) by @Boshen
- remove never-constructed MatchImportKind::_Ignore variant (#10041) by @shulaoda
- remove the unused ScheduledBuild napi struct (#10033) by @shulaoda
- remove dead compute_hmr_update_single method (#10040) by @shulaoda
- drop the redundant visited.insert in manual code splitting (#10038) by @shulaoda
- remove the dead output_assets vector in render_chunk_to_assets (#10036) by @shulaoda
- remove the unused From<String>/Display impls for BindingLogLevel (#10035) by @shulaoda
- deps: upgrade oxc to 0.138.0 and migrate to per-type AST construction (#10018) by @shulaoda
- deps: update rust crates (#9911) by @renovate[bot]
- deps: update test262 submodule for tests (#10016) by @rolldown-guard[bot]
- deps: update github actions (#9999) by @renovate[bot]
- deps: update npm packages (#10000) by @renovate[bot]

### ◀️ Revert

- "fix(plugin): make lazy hook metadata enumerable (#9991)" (#10005) by @shulaoda

### ❤️ New Contributors

* @x1024 made their first contribution in [#9948](#9948)

Co-authored-by: shulaoda <165626830+shulaoda@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants