Format <script> content in glimmer templates (#20012)

Co-authored-by: fisker <lionkay@gmail.com>
This commit is contained in:
Konv Suuandfisker authored and GitHub committed 2026-09-12 20:31:10 +08:00
1 parent 968c1a62c8
commit 2529b92818
14 files changed
+357 -28

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
#### Support embedded `<script>` (#20012 by @kovsu)
<!-- prettier-ignore -->
```hbs
{{! Input }}
<script>
const a = 1;
console.log( a );
</script>
{{! Prettier stable }}
<script>
const a = 1; console.log( a );
</script>
{{! Prettier main }}
<script>
const a = 1;
console.log(a);
</script>
```
+49 -16
View File
@@ -1,4 +1,40 @@
import { isPlainTextStyleElement } from "./utilities.js"; import { isPlainTextStyleOrScriptElement } from "./utilities.js";
function getAttribute(node, name) {
return node.attributes.find(
(attribute) => attribute.type === "AttrNode" && attribute.name === name,
);
}
function getTextValue(attribute) {
return attribute.value.type === "TextNode"
? attribute.value.chars
: undefined;
}
function getScriptTextToDocOptions(node) {
if (getAttribute(node, "src")) {
return;
}
const typeAttribute = getAttribute(node, "type");
const type = typeAttribute ? getTextValue(typeAttribute) : undefined;
if (!typeAttribute || type === "module" || type === "text/javascript") {
return {
parser: "babel",
__embeddedInHtml: true,
__babelSourceType: type === "module" ? "module" : "script",
};
}
}
function getStyleTextToDocOptions(node) {
const langAttribute = getAttribute(node, "lang");
if (!langAttribute || getTextValue(langAttribute) === "css") {
return { parser: "css" };
}
}
function embed(path /* , options*/) { function embed(path /* , options*/) {
const { node } = path; const { node } = path;
@@ -9,30 +45,27 @@ function embed(path /* , options*/) {
const { parent } = path; const { parent } = path;
if (!(isPlainTextStyleElement(parent) && parent.children[0] === node)) { if (!(
isPlainTextStyleOrScriptElement(parent) && parent.children[0] === node
)) {
return; return;
} }
const languageAttribute = parent.attributes.find( const textToDocOptions =
(attribute) => attribute.type === "AttrNode" && attribute.name === "lang", parent.tag === "style"
); ? getStyleTextToDocOptions(parent)
if ( : getScriptTextToDocOptions(parent);
languageAttribute &&
!( if (!textToDocOptions) {
languageAttribute.value.type === "TextNode" &&
languageAttribute.value.chars === "css"
)
) {
return; return;
} }
return async (textToDoc) => { return async (textToDoc) => {
const context = node.chars; const content = node.chars;
if (!context.trim()) { if (!content.trim()) {
return ""; return "";
} }
const doc = await textToDoc(context, { parser: "css" }); return await textToDoc(content, textToDocOptions);
return doc;
}; };
} }
+6 -3
View File
@@ -1,5 +1,5 @@
import htmlWhitespace from "../../utilities/html-whitespace.js"; import htmlWhitespace from "../../utilities/html-whitespace.js";
import { isPlainTextStyleElement } from "../utilities.js"; import { isPlainTextStyleOrScriptElement } from "../utilities.js";
function massageAstNode(original, cloned, parent) { function massageAstNode(original, cloned, parent) {
// (Glimmer/HTML) ignore TextNode // (Glimmer/HTML) ignore TextNode
@@ -9,8 +9,11 @@ function massageAstNode(original, cloned, parent) {
return null; return null;
} }
// CSS will be formatted // CSS/JS will be formatted
if (isPlainTextStyleElement(parent) && parent.children[0] === original) { if (
isPlainTextStyleOrScriptElement(parent) &&
parent.children[0] === original
) {
cloned.chars = ""; cloned.chars = "";
} else { } else {
cloned.chars = htmlWhitespace.split(trimmed).join(" "); cloned.chars = htmlWhitespace.split(trimmed).join(" ");
+10 -6
View File
@@ -20,7 +20,7 @@ import { locEnd, locStart } from "./loc.js";
import { massageAstNode } from "./massage-ast/index.js"; import { massageAstNode } from "./massage-ast/index.js";
import { import {
hasPrettierIgnore, hasPrettierIgnore,
isPlainTextStyleElement, isPlainTextStyleOrScriptElement,
isVoidElement, isVoidElement,
isWhitespaceNode, isWhitespaceNode,
} from "./utilities.js"; } from "./utilities.js";
@@ -60,11 +60,11 @@ function print(path, options, print) {
} }
const endingTag = ["</", node.tag, ">"]; const endingTag = ["</", node.tag, ">"];
const isPlainStyle = isPlainTextStyleElement(node); const isPlainStyleOrScript = isPlainTextStyleOrScriptElement(node);
if ( if (
node.children.length === 0 || node.children.length === 0 ||
((!isWhitespaceSensitive || isPlainStyle) && ((!isWhitespaceSensitive || isPlainStyleOrScript) &&
node.children.every((node) => isWhitespaceNode(node))) node.children.every((node) => isWhitespaceNode(node)))
) { ) {
return [startingTag, endingTag]; return [startingTag, endingTag];
@@ -72,7 +72,7 @@ function print(path, options, print) {
const parts = path.map(print, "children"); const parts = path.map(print, "children");
if (isPlainStyle || !isWhitespaceSensitive) { if (!isWhitespaceSensitive || isPlainStyleOrScript) {
return [startingTag, indent([softline, ...parts]), softline, endingTag]; return [startingTag, indent([softline, ...parts]), softline, endingTag];
} }
@@ -163,7 +163,8 @@ function print(path, options, print) {
case "TextNode": { case "TextNode": {
// Don't format content: // Don't format content:
// 1. in `<pre>`, // 1. in `<pre>`,
// 2. in `<style>` // 2. in `<style>`,
// 3. in `<script>`
let text = node.chars; let text = node.chars;
@@ -173,7 +174,10 @@ function print(path, options, print) {
return replaceEndOfLine(text); return replaceEndOfLine(text);
} }
if (isPlainTextStyleElement(parent)) { if (
isPlainTextStyleOrScriptElement(parent) &&
parent.children[0] === node
) {
text = text.replaceAll(/^\n+/g, ""); text = text.replaceAll(/^\n+/g, "");
text = htmlWhitespace.trimEnd(text); text = htmlWhitespace.trimEnd(text);
text = htmlWhitespace.dedentString(text); text = htmlWhitespace.dedentString(text);
+5 -3
View File
@@ -42,8 +42,10 @@ function isPlainTextElement(node) {
); );
} }
function isPlainTextStyleElement(node) { function isPlainTextStyleOrScriptElement(node) {
return isPlainTextElement(node) && node.tag === "style"; return (
isPlainTextElement(node) && (node.tag === "style" || node.tag === "script")
);
} }
function isPrettierIgnoreNode(node) { function isPrettierIgnoreNode(node) {
@@ -67,7 +69,7 @@ function hasPrettierIgnore(path) {
export { export {
hasPrettierIgnore, hasPrettierIgnore,
isPlainTextStyleElement, isPlainTextStyleOrScriptElement,
isVoidElement, isVoidElement,
isWhitespaceNode, isWhitespaceNode,
}; };
@@ -0,0 +1,181 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`script-tag.hbs format 1`] = `
====================================options=====================================
parsers: ["glimmer"]
printWidth: 80 (default) |
=====================================input======================================
<script></script>
<script> </script>
<script type="module">
import a from "a" ;
</script>
<script>
// Should not support TLA in script without \`[type="module"]\`
await shouldNotFormat ( a );
</script>
<script type="module">
// TLA
await shouldFormat ( a );
</script>
<script type="module">
<!--Should not support HTML comment in \`script[type="module"]\`
shouldNotFormat ( a );
-->
</script>
<script>
<!--html comment
shouldFormat ( a );
-->
</script>
<script>
const template = html\`<div><script>shouldFormat ( )<\\/script></div>\`;
</script>
=====================================output=====================================
<script></script>
<script></script>
<script type="module">
import a from "a";
</script>
<script>
// Should not support TLA in script without \`[type="module"]\`
await shouldNotFormat ( a );
</script>
<script type="module">
// TLA
await shouldFormat(a);
</script>
<script type="module">
<!--Should not support HTML comment in \`script[type="module"]\`
shouldNotFormat ( a );
-->
</script>
<script>
<!--html comment
shouldFormat(a);
-->
</script>
<script>
const template = html\`<div>
<script>
shouldFormat();
<\\/script>
</div>\`;
</script>
================================================================================
`;
exports[`type-attribute.hbs format 1`] = `
====================================options=====================================
parsers: ["glimmer"]
printWidth: 80 (default) |
=====================================input======================================
<script>
shouldFormat ( );
</script>
<script type="text/javascript">
shouldFormat ( );
</script>
<script type>
shouldNotFormat ( );
</script>
<script type="">
shouldNotFormat ( );
</script>
<script type="unknown">
shouldNotFormat ( );
</script>
<script type="application/ld+json">
{ "should" : "not format" }
</script>
=====================================output=====================================
<script>
shouldFormat();
</script>
<script type="text/javascript">
shouldFormat();
</script>
<script type>
shouldNotFormat ( );
</script>
<script type="">
shouldNotFormat ( );
</script>
<script type="unknown">
shouldNotFormat ( );
</script>
<script type="application/ld+json">
{ "should" : "not format" }
</script>
================================================================================
`;
exports[`with-mustache.hbs format 1`] = `
====================================options=====================================
parsers: ["glimmer"]
printWidth: 80 (default) |
=====================================input======================================
<script>
shouldNotFormat( {{foo}} )
</script>
<script>
shouldNotFormat( '{{user.name}}' )
{{#if debug}}shouldNotFormat( 'debug' ){{/if}}
</script>
<script>{{foo}}</script>
=====================================output=====================================
<script>
shouldNotFormat(
{{foo}}
)
</script>
<script>
shouldNotFormat( '{{user.name}}' )
{{#if debug}}shouldNotFormat( 'debug' ){{/if}}
</script>
<script>{{foo}}</script>
================================================================================
`;
exports[`with-src.hbs format 1`] = `
====================================options=====================================
parsers: ["glimmer"]
printWidth: 80 (default) |
=====================================input======================================
<script src="main.js"></script>
<script src="main.js">shouldNotFormat ( );</script>
<script src>shouldNotFormat ( );</script>
<script src="">shouldNotFormat ( );</script>
=====================================output=====================================
<script src="main.js"></script>
<script src="main.js">
shouldNotFormat ( );
</script>
<script src>
shouldNotFormat ( );
</script>
<script src="">
shouldNotFormat ( );
</script>
================================================================================
`;
@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`test.hbs - {"embeddedLanguageFormatting":"off"} format 1`] = `
====================================options=====================================
embeddedLanguageFormatting: "off"
parsers: ["glimmer"]
printWidth: 80 (default) |
=====================================input======================================
<script>
call ( );
</script>
=====================================output=====================================
<script>call ( );</script>
================================================================================
`;
@@ -0,0 +1 @@
runFormatTest(import.meta, ["glimmer"], { embeddedLanguageFormatting: "off" });
@@ -0,0 +1,3 @@
<script>
call ( );
</script>
@@ -0,0 +1 @@
runFormatTest(import.meta, ["glimmer"]);
@@ -0,0 +1,31 @@
<script></script>
<script> </script>
<script type="module">
import a from "a" ;
</script>
<script>
// Should not support TLA in script without `[type="module"]`
await shouldNotFormat ( a );
</script>
<script type="module">
// TLA
await shouldFormat ( a );
</script>
<script type="module">
<!--Should not support HTML comment in `script[type="module"]`
shouldNotFormat ( a );
-->
</script>
<script>
<!--html comment
shouldFormat ( a );
-->
</script>
<script>
const template = html`<div><script>shouldFormat ( )<\/script></div>`;
</script>
@@ -0,0 +1,21 @@
<script>
shouldFormat ( );
</script>
<script type="text/javascript">
shouldFormat ( );
</script>
<script type>
shouldNotFormat ( );
</script>
<script type="">
shouldNotFormat ( );
</script>
<script type="unknown">
shouldNotFormat ( );
</script>
<script type="application/ld+json">
{ "should" : "not format" }
</script>
@@ -0,0 +1,8 @@
<script>
shouldNotFormat( {{foo}} )
</script>
<script>
shouldNotFormat( '{{user.name}}' )
{{#if debug}}shouldNotFormat( 'debug' ){{/if}}
</script>
<script>{{foo}}</script>
@@ -0,0 +1,4 @@
<script src="main.js"></script>
<script src="main.js">shouldNotFormat ( );</script>
<script src>shouldNotFormat ( );</script>
<script src="">shouldNotFormat ( );</script>