While working on a Shopify project, we employed Prettier for formatting our JavaScript, HTML, and CSS files. Shopify however, uses Liquid for templates which Prettier does not support out of the box but fortunately, Shopify developed a Liquid Prettier plugin to handle Liquid files.
When using it, we noticed that the formatting of Prettier on certain Liquid files is not idempotent (formatting it twice should have the same result as formatting it once): in some cases, formatting a Liquid file that has a script tag will result in new lines being appended after the opening script
tag and before the closing script
tag per run. So when you have something like the following in a .liquid file:
<script>
{% if a %}
console.log(`{{ a }}`);
{% endif %}
</script>
LiquidPrettier will format it to
<script>
{% if a %}
console.log(`{{ a }}`);
{% endif %}
</script>
LiquidAnd running Prettier again will make it add more lines. It’s a small but annoying bug that prevents us from automating Prettier.
This bug has been identified and filed in their GitHub repository but since no one has picked it up since March, I want to try to take a stab and fix it.
Generating the minimal test case is to first generate a minimal test case. After experimenting a bit and reducing the sample case in the issue, I came up with the following:
<script>
({{ a }});
``;
</script>
LiquidAnd a counter-example (where the bug does not trigger), notice the use of double quotes instead of backticks:
<script>
({{ a }});
"";
</script>
LiquidFortunately, the prettier-plugin-liquid project has a test suite so it’s just a matter of adding our test case to that and adding a new npm script to just run that specific test case.
Identification
Briefly scanning through the code, we see that the process is split into two parts: parsing and printing. Furthermore, parsing is split into CST (“Concrete Syntax Tree”) generation then converted to an AST. So the question is where the bug is located: in the CST generation? The conversion to AST? or in printing?
A method I used to narrow down the source of the bug is to print the output of each stage, and compare the difference between the test case, which triggers the bug, and the counter-example, which does not trigger the bug. A good place to poke is the cstToAst
function which has access to both CST and AST objects. We can add a logging statement to output both the CST and AST.
Seeing that the CST and AST output of both cases are actually the same (differing only our input), we can conclude that the issue is in the printing.
A good place to look for is the aptly named printRawElement
. And checking the output of generating the body
variable we find the difference:
However, following the code statically will be difficult due to the dynamic callback of path.call(print, 'body')
so we will have to utilize a debugger to help us. Enter the NodeJS Debugger.
NodeJS Debugger
While console.log
and console.trace
gets us far, being able to stop execution and step through what each line of code does at runtime. Fortunately, NodeJS lets you debug programs using Chrome, VS Code, and various other clients.
First is to add a programmatic breakpoint in our code by adding the debugger;
statement.
if (!hasEmptyBody) {
if (shouldIndentBody) {
debugger;
body = [indent([hardline, path.call(print, 'body')]), hardline];
} else {
body = [dedentToRoot([hardline, path.call(print, 'body')]), hardline];
}
}
JavaScriptThen, we modify package.json
to add the --inspect-brk
flag that tells NodeJS to start the debugger and pause execution (just passing --inspect
will result in the program running immediately and possibly exit before one will have a chance to connect to it).
"test:target": "mocha --inspect-brk '{src,test}/issue-171/*.spec.ts'",
JavaScriptFor debugging, I used Chrome and went to chrome://inspect
to access the debugger client, waited a bit in the Devices tab for the target to show up
Once connected, we can use the familiar Chrome debugger to step through the code, since we added the programmatic breakpoint, continuing execution will result in the program pausing at that line:
debugger
statement is.This allows us to step through the code and identify the source of the bug. After tracing through the execution, we can zero-in to this line that just returns the script tag contents verbatim, including the leading and trailing newlines https://github.com/Shopify/prettier-plugin-liquid/blob/main/src/printer/printer-liquid-html.ts#L276 so our solution is to simply return the trimmed string.
Patching Things Up
And to distribute the fix to the rest of the team without having to wait for them to merge the pull request, we can use patch-package to generate and apply the patch with the repository. Doing this is as simple as editing the file directly in node_modules/@shopify/prettier-plugin-liquid/dist/printer/printer-liquid-html.js
then running npx patch-package '@shopify/prettier-plugin-liquid'
to generate the following patch file under the patches
folder, which should be checked-in with the project:
diff --git a/node_modules/@shopify/prettier-plugin-liquid/dist/printer/printer-liquid-html.js b/node_modules/@shopify/prettier-plugin-liquid/dist/printer/printer-liquid-html.js
index 421b76c..e4e68dd 100644
--- a/node_modules/@shopify/prettier-plugin-liquid/dist/printer/printer-liquid-html.js
+++ b/node_modules/@shopify/prettier-plugin-liquid/dist/printer/printer-liquid-html.js
@@ -136,7 +136,7 @@ function printNode(path, options, print, args = {}) {
}
};
if (isRawMarkupIdentationSensitive()) {
- return node.value;
+ return node.value.trim();
}
const lines = (0, utils_2.bodyLines)(node.value);
const rawFirstLineIsntIndented = !!((_a = node.value
PlaintextTo automate the application of the patch when installing the dependencies, add the following entry in the scripts
of your project’s package.json
file
"postinstall": "patch-package"
JSONConclusion
We did an end-to-end walkthrough of using debugging tools to identify and fix bugs in a 3rd-party dependency and then patching the fix for internal teams without needing for upstream maintainers to merge the fix.
Aside from debugging and fixing code, we at Bonito Tech do software development and consultancy. Need help with implementing your project or supplement it with additional expertise? Contact us!