Skip to content

Keep escaped newlines in string literals without --minify-syntax#32577

Open
robobun wants to merge 3 commits into
mainfrom
farm/eb153b5b/preserve-newline-escape-in-strings
Open

Keep escaped newlines in string literals without --minify-syntax#32577
robobun wants to merge 3 commits into
mainfrom
farm/eb153b5b/preserve-newline-escape-in-strings

Conversation

@robobun

@robobun robobun commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes #32576

Problem

bun build converts any string literal containing a \n escape into a backtick template literal with a literal newline, even with no minify flag:

// test.ts
const greeting = "Hello\nWorld";
console.log(greeting);
$ bun build test.ts
var greeting = `Hello
World`;
console.log(greeting);

The template-literal conversion is documented as a minifier feature, so it should not happen unless syntax is being minified.

Cause

best_quote_char_for_string in src/js_printer/lib.rs counts a newline as a cost against single and double quotes but not against backticks. With backtick_cost left at 0, any newline-containing string satisfies backtick_cost < single_cost.min(double_cost) and the quote choice becomes a backtick. This ran unconditionally, independent of the minify options.

Fix

Gate the newline cost on minify_syntax (threaded through best_quote_char_for_string / best_quote_char_for_e_string and their callers). Without --minify-syntax a newline no longer biases toward a backtick, so it stays escaped inside a quoted string. --minify-syntax (and full --minify) keep the existing template-literal output.

This matches esbuild, the reference implementation:

input "Hello\nWorld" esbuild Bun after fix
no flags "Hello\nWorld" "Hello\nWorld"
--minify-whitespace "Hello\nWorld" "Hello\nWorld"
--minify-syntax template (literal newline) template (literal newline)

The newline cost only affected the backtick decision, so strings with both ' and " (no newline) still choose a backtick to avoid escaping, matching esbuild; a string with a " and a newline now picks a single quote with an escaped \n, also matching esbuild.

Verification

$ bun build test.ts
var greeting = "Hello\nWorld";
$ bun build test.ts --minify-whitespace
var greeting="Hello\nWorld";
$ bun build test.ts --minify-syntax
console.log(`Hello
World`);

Tests in test/bundler/bundler_string.test.ts cover the no-minify, --minify-whitespace, and --minify-syntax paths plus the both-quotes and double-quote-with-newline variants. The existing quote-selection assertion in test/bundler/transpiler/transpiler.test.js is updated to the escaped form. New tests fail on the current build and pass with the fix.

bun build converted any string literal containing a \n escape into a
backtick template with a literal newline, even with no minify flag.
best_quote_char_for_string counted a newline as a cost against single
and double quotes but not backticks, so a newline always tipped the
quote choice toward a template literal.

Gate that newline cost on minify_syntax. Without --minify-syntax the
newline now stays escaped inside a quoted string, matching esbuild;
--minify-syntax still produces the template literal.
@robobun

robobun commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:16 PM PT - Jun 21st, 2026

@robobun, your commit 9343b96 has 3 failures in Build #63827 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32577

That installs a local version of the PR into your bun-32577 executable, so you can run:

bun-32577 --bun

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0c2e3e6a-bb21-422a-9137-5e5e2de6f718

📥 Commits

Reviewing files that changed from the base of the PR and between b183590 and c1400b5.

📒 Files selected for processing (1)
  • test/bundler/bundler_string.test.ts

Walkthrough

The JS printer's best_quote_char_for_string function gains a minify_syntax parameter. Newline characters now only increment the single- and double-quote cost counters (which bias the emitter toward backtick/template literals) when minify_syntax is enabled. All call sites in the Printer are updated to pass self.options.minify_syntax, and bundler/transpiler tests are added or corrected to match the new behavior.

Changes

Printer newline-to-template-literal fix

Layer / File(s) Summary
Quote-selection cost model and public signature
src/js_printer/lib.rs
best_quote_char_for_string receives a new minify_syntax: bool parameter. The cost increments for newline characters (which bias selection toward backticks) are now conditional on minify_syntax being true, leaving newlines escaped in non-minified output.
Printer call sites forwarding minify_syntax
src/js_printer/lib.rs
Printer::best_quote_char_for_e_string is updated to accept and forward minify_syntax. Call sites in print_string_literal_e_string, print_string_literal_utf8, UTF-16 property key printing, and import record path printing all pass self.options.minify_syntax.
Bundler and transpiler test updates
test/bundler/bundler_string.test.ts, test/bundler/transpiler/transpiler.test.js
Test imports are updated to include expect. New itBundled cases assert that \n escapes are preserved as "Hello\nWorld" without minifySyntax and emitted as `Hello\nWorld` only with it. The existing transpiler expectation for console.log("\n") is corrected from a template-literal form to a double-quoted escaped string.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Keep escaped newlines in string literals without --minify-syntax' accurately describes the main change: preventing automatic conversion of escaped newlines to template literals unless minify-syntax is enabled.
Description check ✅ Passed The description comprehensively covers the problem, root cause, fix approach, verification steps, and test coverage, following the template structure with both 'What does this PR do?' and 'How did you verify your code works?' sections.
Linked Issues check ✅ Passed The code changes directly address issue #32576 by gating newline cost calculation on minify_syntax flag, ensuring escaped newlines are preserved without minification flags while maintaining template literal behavior with --minify-syntax.
Out of Scope Changes check ✅ Passed All changes in src/js_printer/lib.rs, test/bundler/bundler_string.test.ts, and test/bundler/transpiler/transpiler.test.js are directly related to the scope of fixing escaped newline handling based on minify-syntax settings.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js_printer/lib.rs`:
- Around line 828-836: The issue is that backtick_cost is not being penalized in
non-minify mode when newlines are present, allowing backticks to be selected
over quoted strings even when minify_syntax is false. To fix this, you need to
also increase the backtick_cost when minify_syntax is false in the same
conditional block where single_cost and double_cost are being incremented. This
ensures that in non-minify mode, escaped newlines in quoted strings are
preserved instead of allowing backticks to win and emit literal newlines via
template literals. Modify the if statement that currently only updates
single_cost and double_cost to also update backtick_cost when minify_syntax is
false.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1746a2c3-4228-4eeb-90db-60b1bc226d1e

📥 Commits

Reviewing files that changed from the base of the PR and between 602d956 and b183590.

📒 Files selected for processing (3)
  • src/js_printer/lib.rs
  • test/bundler/bundler_string.test.ts
  • test/bundler/transpiler/transpiler.test.js

Comment thread src/js_printer/lib.rs
A string containing both ' and " selects a backtick to avoid escaping
either quote, independent of minify_syntax and of any newline. esbuild
emits the same, so assert it to document the behavior.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any issues with the change itself — it's a small, well-tested gate that matches esbuild — but CI reported build-rust failures on b183590 across all platforms and the follow-up commit only touches tests, so worth a human confirming CI is green before merging.

Extended reasoning...

Overview

This PR threads a minify_syntax: bool parameter through best_quote_char_for_string / best_quote_char_for_e_string in src/js_printer/lib.rs and gates the newline cost increment on it. Five call sites are updated to pass self.options.minify_syntax. Six new bundler tests in bundler_string.test.ts cover no-minify, --minify-whitespace, --minify-syntax, and the both-quotes / double-quote-with-newline edge cases; one transpiler assertion is updated to the new escaped form. All callers of the changed function are accounted for in the diff.

Security risks

None. This is purely a quote-character selection heuristic in the JS printer — no auth, crypto, filesystem, or network surface. The emitted string value is semantically identical regardless of quote choice; only the textual representation changes.

Level of scrutiny

Moderate. The logic change is tiny (wrap two += 1 lines in an if), mechanical, and verified against esbuild as the reference. CodeRabbit's one concern (both-quotes + newline) was checked against esbuild, found to already match, and a regression test was added; CodeRabbit withdrew the comment. However, this is the core JS printer and the change alters user-visible bun build output for every string literal containing \n in non-minified mode.

Other factors

The robobun CI comment reports build-rust failures on commit b183590 across all Linux/FreeBSD targets. The follow-up commit c1400b5 only adds a test case, so it would not resolve a Rust compile error. The Rust diff itself looks syntactically sound, so this may be transient CI flakiness — but I can't confirm CI is green from here, and that's the main reason I'm deferring rather than approving.

@robobun

robobun commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for flagging. The build-rust failures on b183590 were transient CI infrastructure, not the diff, and they did not recur.

The current run on the latest commit (c1400b5, build #63825) has build-rust green on every platform: Windows x64/aarch64, Linux x64/aarch64 (gnu, musl, android, asan, baseline), FreeBSD x64/aarch64, and macOS x64/aarch64. build-cpp, build-bun, and the Linux x64 ASAN test lane have also passed; the remaining test-bun lanes are still running.

For what it's worth, cargo check -p bun_js_printer and a full bun bd both compiled the change cleanly locally as well.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — focused fix that gates the newline→backtick bias on --minify-syntax, matches esbuild, and is well covered by the new tests.

Extended reasoning...

Overview

This PR threads a minify_syntax: bool parameter through best_quote_char_for_string / best_quote_char_for_e_string in src/js_printer/lib.rs and gates the newline cost increment on it. Without --minify-syntax, a \n in a string literal no longer biases quote selection toward a backtick template literal, so the escape is preserved in the quoted output. All six call sites are updated to forward self.options.minify_syntax. Tests are added in test/bundler/bundler_string.test.ts (six cases covering no-minify, --minify-whitespace, --minify-syntax, both-quotes, both-quotes-with-newline, and double-quote-with-newline) and one expectation in test/bundler/transpiler/transpiler.test.js is corrected.

Security risks

None. This only affects which quote character the printer picks for a string literal; the emitted JavaScript is semantically identical either way ("Hello\nWorld" vs a backtick with a literal newline). No auth, crypto, permissions, or data handling is touched.

Level of scrutiny

Low–medium. While js_printer is production code that affects all bun build output, the change is a narrow heuristic tweak inside a single match arm: wrap two += 1 increments in if minify_syntax { ... }. The remaining cost calculations (for ', ", `, ${) are untouched, so pre-existing quote selection (e.g., backtick when both quote chars are present) is preserved. Two of the updated call sites pass allow_backtick: false anyway, making the new parameter a no-op there. The worst-case failure mode is a different-but-still-valid quote character, not broken output.

Other factors

  • Fixes #32576 and matches esbuild's reference behavior, which the author verified across no-flags / --minify-whitespace / --minify-syntax.
  • The CodeRabbit edge case (both quotes + newline without minify) was investigated, shown to match esbuild, and locked in with the BothQuotesWithNewlineUseBacktickWithoutMinify regression test; that thread is resolved.
  • No CODEOWNERS cover src/js_printer/.
  • The bug-hunting system found no issues, and CI build-rust is green across all platforms per the latest status.

@robobun

robobun commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: diff is green, failing lanes are known flakes

Green on this PR: build-rust, build-cpp, and build-bun pass on every platform (Windows, Linux gnu/musl/android/asan/baseline, FreeBSD, macOS; x64 and aarch64), the new bundler_string / transpiler tests pass, and cargo clippy / Format / Lint JavaScript are green.

This change only selects a different quote character for newline-containing string literals and preserves the string value, so it has no path to TLS, S3, the shell, the V8 heap snapshot, or the installer. The red lanes are pre-existing flaky tests: they vary between my two runs, and the recurring one also fails on unrelated builds.

  • test-tls-client-destroy-soon.js (node TLS, darwin-14-aarch64): also failed on builds 63820 and 63822, which predate this branch, so it is a recurring flake on that platform rather than anything here.
  • s3-stream-cancel-leak.test.ts SIGABRT and, in the prior run, rm.test.ts SIGABRT (debian-13 x64-asan): a different test each run on the ASAN lane.
  • v8-heap-snapshot.test.ts SIGKILL (ubuntu-25.04 x64) and bun-install.test.ts (Windows, auto-retried).

Base commit 602d956 passed full CI (build 63771). I've already re-run CI once; this needs a maintainer to merge or re-trigger.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bun build always converts \n in strings to literal newlines

1 participant