Avoid linebreaks in embedded template interpolations (#18380)

This commit is contained in:
fisker Cheung authored and GitHub committed 2025-12-06 14:45:25 +08:00
1 parent d8bf3989bd
commit a7b8a4f77d
17 files changed
+352 -148

No files matched your search

@@ -0,0 +1,56 @@
#### Fix alignment in embedded template interpolations (#18380 by @fisker)
<!-- prettier-ignore -->
```jsx
// Input
string = `
.class {
flex-direction: column${
long_cond && long_cond && long_cond
? "-reverse" :
""
};
}
`;
css = css`
.class {
flex-direction: column${
long_cond && long_cond && long_cond
? "-reverse" :
""
};
}
`;
// Prettier stable
string = `
.class {
flex-direction: column${
long_cond && long_cond && long_cond ? "-reverse" : ""
};
}
`;
css = css`
.class {
flex-direction: column${long_cond && long_cond && long_cond
? "-reverse"
: ""};
}
`;
// Prettier main
string = `
.class {
flex-direction: column${
long_cond && long_cond && long_cond ? "-reverse" : ""
};
}
`;
css = css`
.class {
flex-direction: column${
long_cond && long_cond && long_cond ? "-reverse" : ""
};
}
`;
```
+34
View File
@@ -0,0 +1,34 @@
#### Avoid linebreaks in embedded template interpolations (#18380 by @fisker)
<!-- prettier-ignore -->
```jsx
// Input
string = /* Comment */ `
<div>${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
`;
html = /* HTML */ `
<div>${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
`;
// Prettier stable
string = /* Comment */ `
<div>${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
`;
html = /* HTML */ `
<div>
${long_cond && long_cond && long_cond && long_cond && long_cond
? "content"
: ""}
</div>
`;
// Prettier main
string = /* Comment */ `
<div>${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
`;
html = /* HTML */ `
<div>
${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}
</div>
`;
```
+2 -2
View File
@@ -11,7 +11,7 @@ import { printTemplateExpressions } from "../print/template-literal.js";
import isNodeMatches from "../utilities/is-node-matches.js";
import { isAngularComponentStyles } from "./utilities.js";
async function printEmbedCss(textToDoc, print, path /* , options*/) {
async function printEmbedCss(textToDoc, print, path, options) {
const { node } = path;
// Get full template literal with expressions replaced by placeholders
@@ -26,7 +26,7 @@ async function printEmbedCss(textToDoc, print, path /* , options*/) {
text += raw;
}
const quasisDoc = await textToDoc(text, { parser: "scss" });
const expressionDocs = printTemplateExpressions(path, print);
const expressionDocs = printTemplateExpressions(path, options, print);
const newDoc = replacePlaceholders(quasisDoc, expressionDocs);
/* c8 ignore next 3 */
if (!newDoc) {
+4 -5
View File
@@ -5,12 +5,12 @@ import {
} from "../print/template-literal.js";
import { hasLanguageComment } from "./utilities.js";
async function printEmbedGraphQL(textToDoc, print, path /* , options*/) {
async function printEmbedGraphQL(textToDoc, print, path, options) {
const { node } = path;
const numQuasis = node.quasis.length;
const expressionDocs = printTemplateExpressions(path, print);
const expressionDocs = printTemplateExpressions(path, options, print);
const parts = [];
for (let i = 0; i < numQuasis; i++) {
@@ -21,7 +21,6 @@ async function printEmbedGraphQL(textToDoc, print, path /* , options*/) {
const lines = text.split("\n");
const numLines = lines.length;
const expressionDoc = expressionDocs[i];
const startsWithBlankLine =
numLines > 2 && lines[0].trim() === "" && lines[1].trim() === "";
@@ -60,8 +59,8 @@ async function printEmbedGraphQL(textToDoc, print, path /* , options*/) {
parts.push("");
}
if (expressionDoc) {
parts.push(expressionDoc);
if (!isLast) {
parts.push(expressionDocs[i]);
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ async function printEmbedHtmlLike(parser, textToDoc, print, path, options) {
)
.join("");
const expressionDocs = printTemplateExpressions(path, print);
const expressionDocs = printTemplateExpressions(path, options, print);
const placeholderRegex = new RegExp(
composePlaceholder(String.raw`(\d+)`),
+124 -113
View File
@@ -28,104 +28,25 @@ import {
* @import {Doc} from "../../document/index.js"
*/
/*
- `TemplateLiteral`
- `TSTemplateLiteralType` (TypeScript)
*/
function printTemplateLiteral(path, options, print) {
const { node } = path;
const isTemplateLiteral = node.type === "TemplateLiteral";
if (isTemplateLiteral && isJestEachTemplateLiteral(path)) {
if (isJestEachTemplateLiteral(path)) {
const printed = printJestEachTemplateLiteral(path, options, print);
if (printed) {
return printed;
}
}
let expressionsKey = "expressions";
if (node.type === "TSTemplateLiteralType") {
expressionsKey = "types";
}
const parts = [];
const expressionDocs = path.map(print, expressionsKey);
const expressionDocs = printTemplateExpressions(path, options, print);
const parts = path.map(
({ isLast, index }) => [print(), isLast ? "" : expressionDocs[index]],
"quasis",
);
parts.push(lineSuffixBoundary, "`");
let previousQuasiIndentSize = 0;
path.each(({ index, node: quasi }) => {
parts.push(print());
if (quasi.tail) {
return;
}
// For a template literal of the following form:
// `someQuery {
// ${call({
// a,
// b,
// })}
// }`
// the expression is on its own line (there is a \n in the previous
// quasi literal), therefore we want to indent the JavaScript
// expression inside at the beginning of ${ instead of the beginning
// of the `.
const { tabWidth } = options;
const text = quasi.value.raw;
const indentSize = text.includes("\n")
? getIndentSize(text, tabWidth)
: previousQuasiIndentSize;
previousQuasiIndentSize = indentSize;
let expressionDoc = expressionDocs[index];
const expression = node[expressionsKey][index];
let interpolationHasNewline = hasNewlineInRange(
options.originalText,
locEnd(quasi),
locStart(node.quasis[index + 1]),
);
if (!interpolationHasNewline) {
// Never add a newline to an interpolation which didn't already have one...
const renderedExpression = printDocToString(expressionDoc, {
...options,
printWidth: Number.POSITIVE_INFINITY,
}).formatted;
// ... unless one will be introduced anyway, e.g. by a nested function.
// This case is rare, so we can pay the cost of re-rendering.
if (renderedExpression.includes("\n")) {
interpolationHasNewline = true;
} else {
expressionDoc = renderedExpression;
}
}
// Breaks at the template element boundaries (${ and }) are preferred to breaking
// in the middle of a MemberExpression
if (
interpolationHasNewline &&
(hasComment(expression) ||
expression.type === "Identifier" ||
isMemberExpression(expression) ||
expression.type === "ConditionalExpression" ||
expression.type === "SequenceExpression" ||
isBinaryCastExpression(expression) ||
isBinaryish(expression))
) {
expressionDoc = [indent([softline, expressionDoc]), softline];
}
const aligned =
indentSize === 0 && text.endsWith("\n")
? align(Number.NEGATIVE_INFINITY, expressionDoc)
: addAlignmentToDoc(expressionDoc, indentSize, tabWidth);
parts.push(group(["${", aligned, lineSuffixBoundary, "}"]));
}, "quasis");
parts.push("`");
return parts;
return [lineSuffixBoundary, "`", ...parts, "`"];
}
function printTaggedTemplateExpression(path, options, print) {
@@ -175,17 +96,15 @@ function printJestEachTemplateLiteral(path, options, print) {
headerNames.some((headerName) => headerName.length > 0)
) {
options.__inJestEach = true;
const expressions = path.map(print, "expressions");
const expressions = printTemplateExpressions(path, options, print);
options.__inJestEach = false;
const stringifiedExpressions = expressions.map(
(doc) =>
"${" +
printDocToString(doc, {
...options,
printWidth: Number.POSITIVE_INFINITY,
endOfLine: "lf",
}).formatted +
"}",
}).formatted,
);
const tableBody = [{ hasLineBreak: false, cells: [] }];
@@ -248,17 +167,108 @@ function printJestEachTemplateLiteral(path, options, print) {
}
}
function printTemplateExpression(path, print) {
const { node } = path;
let printed = print();
if (hasComment(node)) {
printed = group([indent([softline, printed]), softline]);
const templateLiteralIndentCache = new WeakMap();
function getTemplateLiteralExpressionIndent(path, options) {
const { parent: templateLiteral, index } = path;
if (!templateLiteralIndentCache.has(templateLiteral)) {
const { tabWidth } = options;
let previousQuasiIndentSize = 0;
const sizes = templateLiteral.quasis.map((quasi) => {
const text = quasi.value.raw;
const indentSize = text.includes("\n")
? getIndentSize(text, tabWidth)
: previousQuasiIndentSize;
previousQuasiIndentSize = indentSize;
return { indentSize, previousQuasiText: text };
});
templateLiteralIndentCache.set(templateLiteral, sizes);
}
return ["${", printed, lineSuffixBoundary, "}"];
return templateLiteralIndentCache.get(templateLiteral)[index];
}
function printTemplateExpressions(path, print) {
return path.map(() => printTemplateExpression(path, print), "expressions");
/*
- `TemplateLiteral`
- `TSTemplateLiteralType` (TypeScript)
*/
function printTemplateExpression(path, options, print) {
const { node, index } = path;
let expressionDoc = print();
const templateLiteral = path.parent;
const { quasis } = templateLiteral;
const start = locEnd(quasis[index]);
const end = locStart(quasis[index + 1]);
let interpolationHasNewline = hasNewlineInRange(
options.originalText,
start,
end,
);
if (!interpolationHasNewline) {
// Never add a newline to an interpolation which didn't already have one...
const renderedExpression = printDocToString(expressionDoc, {
...options,
printWidth: Number.POSITIVE_INFINITY,
}).formatted;
// ... unless one will be introduced anyway, e.g. by a nested function.
// This case is rare, so we can pay the cost of re-rendering.
if (renderedExpression.includes("\n")) {
interpolationHasNewline = true;
} else {
expressionDoc = renderedExpression;
}
}
// Breaks at the template element boundaries (${ and }) are preferred to breaking
// in the middle of a MemberExpression
if (
interpolationHasNewline &&
(hasComment(node) ||
node.type === "Identifier" ||
isMemberExpression(node) ||
node.type === "ConditionalExpression" ||
node.type === "SequenceExpression" ||
isBinaryCastExpression(node) ||
isBinaryish(node))
) {
expressionDoc = [indent([softline, expressionDoc]), softline];
}
// For a template literal of the following form:
// `someQuery {
// ${call({
// a,
// b,
// })}
// }`
// the expression is on its own line (there is a \n in the previous
// quasi literal), therefore we want to indent the JavaScript
// expression inside at the beginning of ${ instead of the beginning
// of the `.
let { indentSize, previousQuasiText } = getTemplateLiteralExpressionIndent(
path,
options,
);
// In `jest.each`, we know expression will at least indent 2 level
if (options.__inJestEach) {
indentSize = Math.max(indentSize, options.tabWidth);
}
expressionDoc =
indentSize === 0 && previousQuasiText.endsWith("\n")
? align(Number.NEGATIVE_INFINITY, expressionDoc)
: addAlignmentToDoc(expressionDoc, indentSize, options.tabWidth);
return group(["${", expressionDoc, lineSuffixBoundary, "}"]);
}
function printTemplateExpressions(path, options, print) {
return path.map(
() => printTemplateExpression(path, options, print),
path.node.type === "TSTemplateLiteralType" ? "types" : "expressions",
);
}
function escapeTemplateCharacters(doc, raw) {
@@ -277,19 +287,20 @@ function uncookTemplateElementValue(cookedValue) {
return cookedValue.replaceAll(/([\\`]|\$\{)/gu, String.raw`\$1`);
}
/**
* describe.each`table`(name, fn)
* describe.only.each`table`(name, fn)
* describe.skip.each`table`(name, fn)
* test.each`table`(name, fn)
* test.only.each`table`(name, fn)
* test.skip.each`table`(name, fn)
*
* Ref: https://github.com/facebook/jest/pull/6102
*/
const jestEachTriggerRegex = /^[fx]?(?:describe|it|test)$/u;
function isJestEachTemplateLiteral({ node, parent }) {
/**
* describe.each`table`(name, fn)
* describe.only.each`table`(name, fn)
* describe.skip.each`table`(name, fn)
* test.each`table`(name, fn)
* test.only.each`table`(name, fn)
* test.skip.each`table`(name, fn)
*
* Ref: https://github.com/facebook/jest/pull/6102
*/
const jestEachTriggerRegex = /^[fx]?(?:describe|it|test)$/u;
return (
node.type === "TemplateLiteral" &&
parent.type === "TaggedTemplateExpression" &&
parent.quasi === node &&
parent.tag.type === "MemberExpression" &&
+2
View File
@@ -48,6 +48,8 @@ const unstableTests = new Map(
"typescript/union/consistent-with-flow/single-type.ts",
"js/if/non-block.js",
"typescript/import-type/long-module-name/long-module-name4.ts",
// Unstable due to lack of indent information
"js/multiparser-comments/comment-inside.js",
].map((fixture) => {
const [file, isUnstable = () => true] = Array.isArray(fixture)
? fixture
@@ -85,9 +85,9 @@ html\`\${
}\`;
html\`
\${
foo
/* comment */
}
foo
/* comment */
}
\`;
graphql\`
@@ -98,9 +98,9 @@ graphql\`
\`;
graphql\`
\${
foo
/* comment */
}
foo
/* comment */
}
\`;
css\`
@@ -111,9 +111,9 @@ css\`
\`;
css\`
\${
foo
/* comment */
}
foo
/* comment */
}
\`;
markdown\`\${
@@ -229,8 +229,7 @@ const StyledH1 = styled.div\`
justify-content: center;
text-align: center;\`
: ""}
@media (max-width: \${(props) =>
props.noBreakPoint ? "0" : constants.layout.breakpoint.break1}px) {
@media (max-width: \${(props) => (props.noBreakPoint ? "0" : constants.layout.breakpoint.break1)}px) {
font-size: 2em;
}
\`;
@@ -439,9 +439,13 @@ const makeBody = (store, assets, html) =>
}\`
// https://github.com/prettier/prettier/issues/1626#issue-229655106
const Bar = styled.div\`
_ = styled.div\`
color: \${props => (props.highlight.length > 0 ? palette(['text', 'dark', 'tertiary'])(props) : palette(['text', 'dark', 'primary'])(props))} !important;
\`
_ = styled.div\`
color: \${
props => (props.highlight.length > 0 ? palette(['text', 'dark', 'tertiary'])(props) : palette(['text', 'dark', 'primary'])(props))} !important;
\`
// https://github.com/prettier/prettier/issues/3368
let message = \`this is a long message which contains an interpolation: \${format(data)} <- like this\`;
@@ -564,7 +568,10 @@ const makeBody = (store, assets, html) =>
)}\`;
// https://github.com/prettier/prettier/issues/1626#issue-229655106
const Bar = styled.div\`
_ = styled.div\`
color: \${(props) => (props.highlight.length > 0 ? palette(["text", "dark", "tertiary"])(props) : palette(["text", "dark", "primary"])(props))} !important;
\`;
_ = styled.div\`
color: \${(props) =>
props.highlight.length > 0
? palette(["text", "dark", "tertiary"])(props)
@@ -652,9 +659,13 @@ const makeBody = (store, assets, html) =>
}\`
// https://github.com/prettier/prettier/issues/1626#issue-229655106
const Bar = styled.div\`
_ = styled.div\`
color: \${props => (props.highlight.length > 0 ? palette(['text', 'dark', 'tertiary'])(props) : palette(['text', 'dark', 'primary'])(props))} !important;
\`
_ = styled.div\`
color: \${
props => (props.highlight.length > 0 ? palette(['text', 'dark', 'tertiary'])(props) : palette(['text', 'dark', 'primary'])(props))} !important;
\`
// https://github.com/prettier/prettier/issues/3368
let message = \`this is a long message which contains an interpolation: \${format(data)} <- like this\`;
@@ -777,7 +788,10 @@ const makeBody = (store, assets, html) =>
)}\`;
// https://github.com/prettier/prettier/issues/1626#issue-229655106
const Bar = styled.div\`
_ = styled.div\`
color: \${(props) => (props.highlight.length > 0 ? palette(["text", "dark", "tertiary"])(props) : palette(["text", "dark", "primary"])(props))} !important;
\`;
_ = styled.div\`
color: \${(props) =>
props.highlight.length > 0
? palette(["text", "dark", "tertiary"])(props)
+5 -1
View File
@@ -52,9 +52,13 @@ const makeBody = (store, assets, html) =>
}`
// https://github.com/prettier/prettier/issues/1626#issue-229655106
const Bar = styled.div`
_ = styled.div`
color: ${props => (props.highlight.length > 0 ? palette(['text', 'dark', 'tertiary'])(props) : palette(['text', 'dark', 'primary'])(props))} !important;
`
_ = styled.div`
color: ${
props => (props.highlight.length > 0 ? palette(['text', 'dark', 'tertiary'])(props) : palette(['text', 'dark', 'primary'])(props))} !important;
`
// https://github.com/prettier/prettier/issues/3368
let message = `this is a long message which contains an interpolation: ${format(data)} <- like this`;
@@ -111,6 +111,58 @@ const TestComponent = ({ children, ...props }) => (
================================================================================
`;
exports[`expression-break.js format 1`] = `
====================================options=====================================
parsers: ["babel", "flow", "typescript"]
printWidth: 80
| printWidth
=====================================input======================================
// https://github.com/prettier/prettier/issues/18369
_ = \`
<div>\${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
\`;
_ = /* comment */ \`
<div>\${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
\`;
_ = /* HTML */ \`
<div>\${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
\`;
_ = unknown\`
<div>\${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
\`;
_ = html\`
<div>\${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
\`;
=====================================output=====================================
// https://github.com/prettier/prettier/issues/18369
_ = \`
<div>\${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
\`;
_ = /* comment */ \`
<div>\${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
\`;
_ = /* HTML */ \`
<div>
\${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}
</div>
\`;
_ = unknown\`
<div>\${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
\`;
_ = html\`
<div>
\${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}
</div>
\`;
================================================================================
`;
exports[`expressions.js format 1`] = `
====================================options=====================================
parsers: ["babel", "flow", "typescript"]
@@ -0,0 +1,18 @@
// https://github.com/prettier/prettier/issues/18369
_ = `
<div>${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
`;
_ = /* comment */ `
<div>${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
`;
_ = /* HTML */ `
<div>${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
`;
_ = unknown`
<div>${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
`;
_ = html`
<div>${long_cond && long_cond && long_cond && long_cond && long_cond ? "content" : ""}</div>
`;
@@ -657,9 +657,9 @@ describe.each\`
describe.each\`
a | b | expected
\${11111111111} | \${a()
.b(x => x)
.c()
.d()} | \${2}
.b(x => x)
.c()
.d()} | \${2}
\${1} | \${2} | \${3}
\${2} | \${1} | \${3}
\`;
@@ -801,9 +801,9 @@ describe.each\`
describe.each\`
a | b | expected
\${11111111111} | \${a()
.b((x) => x)
.c()
.d()} | \${2}
.b((x) => x)
.c()
.d()} | \${2}
\${1} | \${2} | \${3}
\${2} | \${1} | \${3}
\`;
@@ -2,7 +2,7 @@
exports[`styled-components.js format 1`] = `
====================================options=====================================
parsers: ["flow"]
parsers: ["babel", "typescript", "flow"]
printWidth: 80
| printWidth
=====================================input======================================
@@ -33,6 +33,11 @@ printWidth: 80
: ""};
}
\`}</style>;
<style jsx>{\`
.class {
flex-direction: column\${long_cond && long_cond && long_cond? "-reverse": ""};
}
\`}</style>;
=====================================output=====================================
<style jsx>{\`
@@ -58,9 +63,14 @@ printWidth: 80
// #5886
<style jsx>{\`
.class {
flex-direction: column\${long_cond && long_cond && long_cond
? "-reverse"
: ""};
flex-direction: column\${
long_cond && long_cond && long_cond ? "-reverse" : ""
};
}
\`}</style>;
<style jsx>{\`
.class {
flex-direction: column\${long_cond && long_cond && long_cond ? "-reverse" : ""};
}
\`}</style>;
+1 -1
View File
@@ -1 +1 @@
runFormatTest(import.meta, ["flow"]);
runFormatTest(import.meta, ["babel", "typescript", "flow"]);
@@ -25,3 +25,8 @@
: ""};
}
`}</style>;
<style jsx>{`
.class {
flex-direction: column${long_cond && long_cond && long_cond? "-reverse": ""};
}
`}</style>;