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 625c6a413..54f332b64 100644 --- a/tests/config/format-test/failed-format-tests.js +++ b/tests/config/format-test/failed-format-tests.js @@ -25,8 +25,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` +
  1. + ${entry.children + ? html` +
      + ${entry.children.map( + (child) => html`
    1. ${child.title}
    2. `, + )} +
    + ` + : entry.title} +
  2. + `, + )} +
+`; + +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\` +
  1. + \${entry.children + ? html\` +
      + \${entry.children.map( + (child) => html\`
    1. \${child.title}
    2. \`, + )} +
    + \` + : entry.title} +
  2. + \`, + )} +
+\`; + +export function foo() { + return html\` +
+
\${JSON.stringify({
+                  a: 1,
+                  b: 2,
+                })}
+
+ \`; +} + +const a = html\` + \${{ + c: y, + }} +\`; + +=====================================output===================================== +const t = html\` +
    + \${items.map( + (entry) => html\` +
  1. + \${ + entry.children + ? html\` +
      + \${entry.children.map( + (child) => html\`
    1. \${child.title}
    2. \`, + )} +
    + \` + : entry.title + } +
  2. + \`, + )} +
+\`; + +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 + } +
+
+ \${ + 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 + } +
+
+ ${ + 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 9eeb77132..212d9cbc6 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"] ;