diff --git a/crates/swc/tests/fixture/codegen/jsx-1/output/index.js b/crates/swc/tests/fixture/codegen/jsx-1/output/index.js index 6d260ba40c48..4b2af551d9fa 100644 --- a/crates/swc/tests/fixture/codegen/jsx-1/output/index.js +++ b/crates/swc/tests/fixture/codegen/jsx-1/output/index.js @@ -1,5 +1,5 @@ export default /*#__PURE__*/ React.createElement(A, { className: b, header: "C", - subheader: "D E" + subheader: "D\n E" }); diff --git a/crates/swc/tests/fixture/issues-1xxx/1233/case-1/output/index.js b/crates/swc/tests/fixture/issues-1xxx/1233/case-1/output/index.js index aecbb52f1641..22f2a06652d3 100644 --- a/crates/swc/tests/fixture/issues-1xxx/1233/case-1/output/index.js +++ b/crates/swc/tests/fixture/issues-1xxx/1233/case-1/output/index.js @@ -1,5 +1,5 @@ function Component() { return /*#__PURE__*/ React.createElement("div", { - name: "A B" + name: "A\n B" }); } diff --git a/crates/swc/tests/fixture/issues-2xxx/2162/case4/output/index.js b/crates/swc/tests/fixture/issues-2xxx/2162/case4/output/index.js index b49faaa854d3..b7f5b91ae7f8 100644 --- a/crates/swc/tests/fixture/issues-2xxx/2162/case4/output/index.js +++ b/crates/swc/tests/fixture/issues-2xxx/2162/case4/output/index.js @@ -1,5 +1,5 @@ function test() { return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(A, { - b: "\\ " + b: "\\\n " })); } diff --git a/crates/swc_ecma_parser/src/lexer/mod.rs b/crates/swc_ecma_parser/src/lexer/mod.rs index 939de86d8c86..959ccbccf8ec 100644 --- a/crates/swc_ecma_parser/src/lexer/mod.rs +++ b/crates/swc_ecma_parser/src/lexer/mod.rs @@ -1502,7 +1502,10 @@ impl<'a> Lexer<'a> { } } - chunk_start = cur_pos + BytePos(ch.len_utf8() as _); + // `read_jsx_new_line` can consume an entire CRLF sequence, so + // restart from the actual current position instead of advancing + // by only the first code unit. + chunk_start = self.input().cur_pos(); } else { self.bump(ch.len_utf8()); } diff --git a/crates/swc_ecma_parser/src/parser/jsx.rs b/crates/swc_ecma_parser/src/parser/jsx.rs index 9ddccba67b5a..f19c41f02da5 100644 --- a/crates/swc_ecma_parser/src/parser/jsx.rs +++ b/crates/swc_ecma_parser/src/parser/jsx.rs @@ -581,6 +581,37 @@ mod tests { ); } + #[test] + fn crlf_in_attr() { + assert_eq_ignore_span!( + jsx(concat!( + "
" + )), + Box::new(Expr::JSXElement(Box::new(JSXElement { + span, + opening: JSXOpeningElement { + span, + attrs: vec![JSXAttrOrSpread::JSXAttr(JSXAttr { + span, + name: JSXAttrName::Ident(IdentName::new(atom!("data-anything"), span)), + value: Some(JSXAttrValue::Str(Str { + span, + value: atom!("line1\r\n line2").into(), + raw: Some(Atom::new(concat!("\"line1", "\r\n", " line2\""))), + })), + })], + name: JSXElementName::Ident(Ident::new_no_ctxt(atom!("div"), span)), + self_closing: true, + type_args: None, + }, + children: Vec::new(), + closing: None + }))) + ); + } + #[test] fn issue_584() { assert_eq_ignore_span!( diff --git a/crates/swc_ecma_transforms_react/src/jsx/mod.rs b/crates/swc_ecma_transforms_react/src/jsx/mod.rs index 5d69d312ffc4..49b2ae40a051 100644 --- a/crates/swc_ecma_transforms_react/src/jsx/mod.rs +++ b/crates/swc_ecma_transforms_react/src/jsx/mod.rs @@ -2192,9 +2192,10 @@ fn transform_jsx_attr_str(v: &Wtf8) -> Wtf8Buf { '\u{000c}' => buf.push_str("\\f"), ' ' => buf.push_char(' '), - '\n' | '\r' | '\t' => { + '\n' => buf.push_char('\n'), + '\r' => buf.push_char('\r'), + '\t' => { buf.push_char(' '); - while let Some(next) = iter.peek() { if next.to_char() == Some(' ') { iter.next(); diff --git a/crates/swc_ecma_transforms_react/src/jsx/tests.rs b/crates/swc_ecma_transforms_react/src/jsx/tests.rs index 334f34c3f877..cdd935c4a4fa 100644 --- a/crates/swc_ecma_transforms_react/src/jsx/tests.rs +++ b/crates/swc_ecma_transforms_react/src/jsx/tests.rs @@ -11,7 +11,9 @@ use swc_ecma_transforms_base::{fixer::fixer, hygiene, resolver}; use swc_ecma_transforms_compat::es2015::{arrow, classes}; #[cfg(feature = "es3")] use swc_ecma_transforms_compat::es3::property_literals; -use swc_ecma_transforms_testing::{parse_options, test, test_fixture, FixtureTestConfig, Tester}; +use swc_ecma_transforms_testing::{ + parse_options, test, test_fixture, test_inline, FixtureTestConfig, Tester, +}; use testing::NormalizedOutput; use super::*; @@ -1302,6 +1304,57 @@ test!( " ); +// Keep CR / CRLF coverage inline because fixture files in this repository are +// normalized to LF. +test_inline!( + Syntax::Es(EsSyntax { + jsx: true, + ..Default::default() + }), + |t| tr(t, Default::default(), Mark::fresh(Mark::root())), + jsx_attr_string_preserves_carriage_return, + concat!( + "const element =
;" + ), + "const element = /*#__PURE__*/ React.createElement(\"div\", {\n \"data-anything\": \ + \"line1\\r line2\"\n});" +); + +test_inline!( + Syntax::Es(EsSyntax { + jsx: true, + ..Default::default() + }), + |t| tr(t, Default::default(), Mark::fresh(Mark::root())), + jsx_attr_string_preserves_crlf, + concat!( + "const element =
;" + ), + "const element = /*#__PURE__*/ React.createElement(\"div\", {\n \"data-anything\": \ + \"line1\\r\\n line2\"\n});" +); + +test_inline!( + Syntax::Es(EsSyntax { + jsx: true, + ..Default::default() + }), + |t| tr(t, Default::default(), Mark::fresh(Mark::root())), + jsx_attr_string_preserves_repeated_crlf_and_collapses_tabs, + concat!( + "const element =
;" + ), + "const element = /*#__PURE__*/ React.createElement(\"div\", {\n \"data-anything\": \ + \"line1\\r\\n\\r\\n line2\"\n});" +); + #[testing::fixture("tests/jsx/fixture/**/input.js")] fn fixture(input: PathBuf) { let mut output = input.with_file_name("output.js"); diff --git a/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11550/input.js b/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11550/input.js new file mode 100644 index 000000000000..b35ecf0b3c89 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11550/input.js @@ -0,0 +1,4 @@ +// Newline in quoted JSX attribute value should be escaped, not collapsed to space +// https://github.com/swc-project/swc/issues/11550 +const hello =
hello
; diff --git a/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11550/output.mjs b/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11550/output.mjs new file mode 100644 index 000000000000..0ec884351619 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11550/output.mjs @@ -0,0 +1,5 @@ +// Newline in quoted JSX attribute value should be escaped, not collapsed to space +// https://github.com/swc-project/swc/issues/11550 +const hello = /*#__PURE__*/ React.createElement("div", { + "data-anything": "bruh\nbruh" +}, "hello"); diff --git a/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11728-mixed-whitespace/input.js b/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11728-mixed-whitespace/input.js new file mode 100644 index 000000000000..d390d6a8e30c --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11728-mixed-whitespace/input.js @@ -0,0 +1,3 @@ +// Newlines should be preserved while tabs still collapse to a single space. +const hello =
hello
; diff --git a/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11728-mixed-whitespace/output.mjs b/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11728-mixed-whitespace/output.mjs new file mode 100644 index 000000000000..c1f214151dcd --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11728-mixed-whitespace/output.mjs @@ -0,0 +1,4 @@ +// Newlines should be preserved while tabs still collapse to a single space. +const hello = /*#__PURE__*/ React.createElement("div", { + "data-anything": "line1\n line2" +}, "hello"); diff --git a/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11728-multiple-line-breaks/input.js b/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11728-multiple-line-breaks/input.js new file mode 100644 index 000000000000..dd55bb152637 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11728-multiple-line-breaks/input.js @@ -0,0 +1,5 @@ +// Multiple literal line breaks inside quoted JSX attribute strings should stay intact. +const hello =
hello
; diff --git a/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11728-multiple-line-breaks/output.mjs b/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11728-multiple-line-breaks/output.mjs new file mode 100644 index 000000000000..7f952f8be829 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/jsx/fixture/issue-11728-multiple-line-breaks/output.mjs @@ -0,0 +1,4 @@ +// Multiple literal line breaks inside quoted JSX attribute strings should stay intact. +const hello = /*#__PURE__*/ React.createElement("div", { + "data-anything": "line1\n\n line2\nline3" +}, "hello"); diff --git a/crates/swc_ecma_transforms_react/tests/jsx/fixture/vercel/1/output.mjs b/crates/swc_ecma_transforms_react/tests/jsx/fixture/vercel/1/output.mjs index 6d260ba40c48..4b2af551d9fa 100644 --- a/crates/swc_ecma_transforms_react/tests/jsx/fixture/vercel/1/output.mjs +++ b/crates/swc_ecma_transforms_react/tests/jsx/fixture/vercel/1/output.mjs @@ -1,5 +1,5 @@ export default /*#__PURE__*/ React.createElement(A, { className: b, header: "C", - subheader: "D E" + subheader: "D\n E" });