diff --git a/changelog_unreleased/javascript/19725.md b/changelog_unreleased/javascript/19725.md
new file mode 100644
index 000000000..088f037de
--- /dev/null
+++ b/changelog_unreleased/javascript/19725.md
@@ -0,0 +1,38 @@
+#### Fix embedded template literal idempotency (#19725 by @fisker)
+
+
+```jsx
+// Input
+const foo = html`
+
${getText({
+ value,
+ })}
+`;
+
+// Prettier stable (first format)
+const foo = html`
+
+ ${getText({
+ value,
+ })}
+
+`;
+
+// Prettier stable (second format)
+const foo = html`
+
+ ${getText({
+ value,
+ })}
+
+`;
+
+// Prettier main
+const foo = html`
+
+ ${getText({
+ value,
+ })}
+
+`;
+```
diff --git a/src/language-js/embed/css.js b/src/language-js/embed/css.js
index f3171e760..baaa60b51 100644
--- a/src/language-js/embed/css.js
+++ b/src/language-js/embed/css.js
@@ -7,7 +7,7 @@ import {
softline,
} from "../../document/index.js";
import isNonEmptyArray from "../../utilities/is-non-empty-array.js";
-import { printTemplateExpressions } from "../print/template-literal.js";
+import { printEmbeddedTemplateExpressions } from "../print/template-literal.js";
import { isNodeMatches } from "../utilities/is-node-matches.js";
import { isAngularComponentStyles } from "./utilities.js";
@@ -26,7 +26,7 @@ async function printEmbedCss(textToDoc, print, path, options) {
text += raw;
}
const quasisDoc = await textToDoc(text, { parser: "scss" });
- const expressionDocs = printTemplateExpressions(path, options, print);
+ const expressionDocs = printEmbeddedTemplateExpressions(path, options, print);
const newDoc = replacePlaceholders(quasisDoc, expressionDocs);
/* c8 ignore next 3 */
if (!newDoc) {
diff --git a/src/language-js/embed/graphql.js b/src/language-js/embed/graphql.js
index 4d98a5ab4..02421933f 100644
--- a/src/language-js/embed/graphql.js
+++ b/src/language-js/embed/graphql.js
@@ -1,7 +1,7 @@
import { hardline, indent, join } from "../../document/index.js";
import {
escapeTemplateCharacters,
- printTemplateExpressions,
+ printEmbeddedTemplateExpressions,
} from "../print/template-literal.js";
import { hasLanguageComment } from "./utilities.js";
@@ -10,7 +10,7 @@ async function printEmbedGraphQL(textToDoc, print, path, options) {
const numQuasis = node.quasis.length;
- const expressionDocs = printTemplateExpressions(path, options, print);
+ const expressionDocs = printEmbeddedTemplateExpressions(path, options, print);
const parts = [];
for (let i = 0; i < numQuasis; i++) {
diff --git a/src/language-js/embed/html.js b/src/language-js/embed/html.js
index e2ac60357..ce4f6bb8d 100644
--- a/src/language-js/embed/html.js
+++ b/src/language-js/embed/html.js
@@ -7,7 +7,7 @@ import {
mapDoc,
} from "../../document/index.js";
import {
- printTemplateExpressions,
+ printEmbeddedTemplateExpressions,
uncookTemplateElementValue,
} from "../print/template-literal.js";
import { hasLanguageComment, isAngularComponentTemplate } from "./utilities.js";
@@ -30,7 +30,7 @@ async function printEmbedHtmlLike(parser, textToDoc, print, path, options) {
)
.join("");
- const expressionDocs = printTemplateExpressions(path, options, print);
+ const expressionDocs = printEmbeddedTemplateExpressions(path, options, print);
const placeholderRegex = new RegExp(
composePlaceholder(String.raw`(\d+)`),
diff --git a/src/language-js/print/template-literal.js b/src/language-js/print/template-literal.js
index dbb668b1c..e84e235ed 100644
--- a/src/language-js/print/template-literal.js
+++ b/src/language-js/print/template-literal.js
@@ -203,7 +203,12 @@ function getTemplateLiteralExpressionIndent(path, options) {
- `TemplateLiteral`
- `TSTemplateLiteralType` (TypeScript)
*/
-function printTemplateExpression(path, options, print) {
+function printTemplateExpression(
+ path,
+ options,
+ print,
+ shouldPreserveIndentation = true,
+) {
const { node, index } = path;
let expressionDoc = print();
@@ -249,6 +254,11 @@ function printTemplateExpression(path, options, print) {
expressionDoc = [indent([softline, expressionDoc]), softline];
}
+ // There is no way to know how the quasis will be printed
+ if (!shouldPreserveIndentation) {
+ return group(["${", expressionDoc, lineSuffixBoundary, "}"]);
+ }
+
// For a template literal of the following form:
// `someQuery {
// ${call({
@@ -283,6 +293,19 @@ function printTemplateExpressions(path, options, print) {
);
}
+function printEmbeddedTemplateExpressions(path, options, print) {
+ return path.map(
+ () =>
+ printTemplateExpression(
+ path,
+ options,
+ print,
+ /* shouldPreserveIndentation */ false,
+ ),
+ "expressions",
+ );
+}
+
function escapeTemplateCharacters(doc, raw) {
return mapDoc(doc, (currentDoc) => {
if (typeof currentDoc === "string") {
@@ -331,8 +354,8 @@ function isJestEachTemplateLiteral({ node, parent }) {
export {
escapeTemplateCharacters,
+ printEmbeddedTemplateExpressions,
printTaggedTemplateExpression,
- printTemplateExpressions,
printTemplateLiteral,
uncookTemplateElementValue,
};
diff --git a/tests/config/format-test/failed-format-tests.js b/tests/config/format-test/failed-format-tests.js
index df1e444fc..bbc8b42ee 100644
--- a/tests/config/format-test/failed-format-tests.js
+++ b/tests/config/format-test/failed-format-tests.js
@@ -24,8 +24,6 @@ const unstableTests = new Map(
"typescript/satisfies-operators/comments-unstable.ts",
"jsx/comments/in-attributes.js",
"typescript/import-type/long-module-name/long-module-name4.ts",
- // Unstable due to lack of indent information
- "js/multiparser-comments/comment-inside.js",
[
"typescript/method-chain/object/issue-17239.ts",
(options) => options.objectWrap !== "collapse",
diff --git a/tests/format/js/embeded/indention/19518.js b/tests/format/js/embeded/indention/19518.js
new file mode 100644
index 000000000..0f3a45acb
--- /dev/null
+++ b/tests/format/js/embeded/indention/19518.js
@@ -0,0 +1,36 @@
+const t = html`
+
+ ${items.map(
+ (entry) => html`
+ -
+ ${entry.children
+ ? html`
+
+ ${entry.children.map(
+ (child) => html`- ${child.title}
`,
+ )}
+
+ `
+ : entry.title}
+
+ `,
+ )}
+
+`;
+
+export function foo() {
+ return html`
+
+
${JSON.stringify({
+ a: 1,
+ b: 2,
+ })}
+
+ `;
+}
+
+const a = html`
+ ${{
+ c: y,
+ }}
+`;
diff --git a/tests/format/js/embeded/indention/__snapshots__/format.test.js.snap b/tests/format/js/embeded/indention/__snapshots__/format.test.js.snap
new file mode 100644
index 000000000..4ff6e4d96
--- /dev/null
+++ b/tests/format/js/embeded/indention/__snapshots__/format.test.js.snap
@@ -0,0 +1,236 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`19518.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+ printWidth: 80 (default) |
+=====================================input======================================
+const t = html\`
+
+ \${items.map(
+ (entry) => html\`
+ -
+ \${entry.children
+ ? html\`
+
+ \${entry.children.map(
+ (child) => html\`- \${child.title}
\`,
+ )}
+
+ \`
+ : entry.title}
+
+ \`,
+ )}
+
+\`;
+
+export function foo() {
+ return html\`
+
+
\${JSON.stringify({
+ a: 1,
+ b: 2,
+ })}
+
+ \`;
+}
+
+const a = html\`
+ \${{
+ c: y,
+ }}
+\`;
+
+=====================================output=====================================
+const t = html\`
+
+ \${items.map(
+ (entry) => html\`
+ -
+ \${
+ entry.children
+ ? html\`
+
+ \${entry.children.map(
+ (child) => html\`- \${child.title}
\`,
+ )}
+
+ \`
+ : entry.title
+ }
+
+ \`,
+ )}
+
+\`;
+
+export function foo() {
+ return html\`
+
+
+\${JSON.stringify({
+ a: 1,
+ b: 2,
+ })}
+
+ \`;
+}
+
+const a = html\`
+ \${{
+ c: y,
+ }}
+\`;
+
+================================================================================
+`;
+
+exports[`indention.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+ printWidth: 80 (default) |
+=====================================input======================================
+_ = \`
+ line
+ \${
+ a
+ // comment
+ + b}
+ line
+\`;
+
+_ = String.raw\`
+ line
+ \${
+ a
+ // comment
+ + b}
+ line
+\`;
+
+_ = css\`
+ a{
+ color:
+ \${
+ a
+ // comment
+ + b}
+ ;
+ }
+\`;
+
+_ = html\`
+
+ \${
+ a
+ // comment
+ + b}
+
+\`;
+
+_ = gql\`
+
+ \${
+ a
+ // comment
+ + b}
+
+\`;
+
+=====================================output=====================================
+_ = \`
+ line
+ \${
+ a +
+ // comment
+ b
+ }
+ line
+\`;
+
+_ = String.raw\`
+ line
+ \${
+ a +
+ // comment
+ b
+ }
+ line
+\`;
+
+_ = css\`
+ a {
+ color: \${
+ a +
+ // comment
+ b
+ };
+ }
+\`;
+
+_ = html\`
+
+ \${
+ a +
+ // comment
+ b
+ }
+
+\`;
+
+_ = gql\`
+ \${
+ a +
+ // comment
+ b
+ }
+\`;
+
+================================================================================
+`;
+
+exports[`indention-2.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+ printWidth: 80 (default) |
+=====================================input======================================
+_ = html\`
+
+ \${
+ a + //
+ b
+ }
+
+
+\`
+
+=====================================output=====================================
+_ = html\`
+
+ \${
+ a + //
+ b
+ }
+
+
+
+
+
+ \${
+ a + //
+ b
+ }
+
+
+
+
+\`;
+
+================================================================================
+`;
diff --git a/tests/format/js/embeded/indention/format.test.js b/tests/format/js/embeded/indention/format.test.js
new file mode 100644
index 000000000..67c251cce
--- /dev/null
+++ b/tests/format/js/embeded/indention/format.test.js
@@ -0,0 +1 @@
+runFormatTest(import.meta, ["babel", "flow", "typescript"]);
diff --git a/tests/format/js/embeded/indention/indention-2.js b/tests/format/js/embeded/indention/indention-2.js
new file mode 100644
index 000000000..587876202
--- /dev/null
+++ b/tests/format/js/embeded/indention/indention-2.js
@@ -0,0 +1,14 @@
+_ = html`
+
+ ${
+ a + //
+ b
+ }
+
+
+`
diff --git a/tests/format/js/embeded/indention/indention.js b/tests/format/js/embeded/indention/indention.js
new file mode 100644
index 000000000..39c45a0c6
--- /dev/null
+++ b/tests/format/js/embeded/indention/indention.js
@@ -0,0 +1,46 @@
+_ = `
+ line
+ ${
+ a
+ // comment
+ + b}
+ line
+`;
+
+_ = String.raw`
+ line
+ ${
+ a
+ // comment
+ + b}
+ line
+`;
+
+_ = css`
+ a{
+ color:
+ ${
+ a
+ // comment
+ + b}
+ ;
+ }
+`;
+
+_ = html`
+
+ ${
+ a
+ // comment
+ + b}
+
+`;
+
+_ = gql`
+
+ ${
+ a
+ // comment
+ + b}
+
+`;
diff --git a/tests/format/js/multiparser-comments/__snapshots__/format.test.js.snap b/tests/format/js/multiparser-comments/__snapshots__/format.test.js.snap
index efbfd4a6a..bfe5a488d 100644
--- a/tests/format/js/multiparser-comments/__snapshots__/format.test.js.snap
+++ b/tests/format/js/multiparser-comments/__snapshots__/format.test.js.snap
@@ -84,9 +84,9 @@ html\`\${
}\`;
html\`
\${
- foo
- /* comment */
-}
+ foo
+ /* comment */
+ }
\`;
graphql\`
@@ -97,9 +97,9 @@ graphql\`
\`;
graphql\`
\${
- foo
- /* comment */
-}
+ foo
+ /* comment */
+ }
\`;
css\`
@@ -110,9 +110,9 @@ css\`
\`;
css\`
\${
- foo
- /* comment */
-}
+ foo
+ /* comment */
+ }
\`;
markdown\`\${
diff --git a/tests/format/jsx/template/__snapshots__/format.test.js.snap b/tests/format/jsx/template/__snapshots__/format.test.js.snap
index c9ca79178..deead2323 100644
--- a/tests/format/jsx/template/__snapshots__/format.test.js.snap
+++ b/tests/format/jsx/template/__snapshots__/format.test.js.snap
@@ -63,8 +63,8 @@ parsers: ["babel", "typescript", "flow"]
;