Prettier - From Vision to Architecture
Prettier is an opinionated code formatter for JavaScript and many other web languages. It was released in 2017 and now about 60% of JavaScript developers used it regularly in 20201 2. We have previously written a post about Prettier’s vision and the problem it tries to solve. We will dive into the project’s architecture in this post.
Main architectural patterns
We start our analysis by getting an overview of the system. This is done by describing the main architectural patterns applied and the main execution environments of Prettier.
To understand what an architectural pattern is, we first have to understand what software architecture is about and what a pattern is. In lecture two3 we learned that Software Architecture can be defined in multiple ways, IEEE describes it as:
The set of fundamental concepts or properties of the system in its environment, embodied in its elements and relationships, and the principles of its design and evolution.
In the book A Pattern Language4, the authors define patterns as:
A pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to the problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice.
More succinctly, patterns are about capturing and communicating solutions to problems in a reusable form. Thus architectural patterns are about reusable solutions to common problems in software architecture5. Examples of architectural patterns are model-view-controller, layered architecture, and REST6.
Prettier applies a pattern called pipes and filters architecture. It is best thought of as a pipeline of successive transformations, in which a data stream enters a filter through its input port and is piped to the next one through its output port7. The figure above illustrates this, where the source produces data, the sink consumes it, and the filters transform it. This pattern is commonly used in Unix tools and in compiler construction7 8. In fact, a compiler has some parts in common with Prettier. A compiler usually starts by parsing the source code into an abstract syntax tree (AST), performs transformations to create an intermediate representation (IR), and finally produces an output (often machine code)8.
For Prettier, the output language is the same as the language of the source code. This process is illustrated in the figure above9.
Containers view
It is useful to employ various views when describing a system’s architecture. We begin by discussing Prettier in the context view. The purpose of the context view is to give a high-level overview of the systems and the execution environments they can be run in.
In its simplest form, Prettier is available as a shell script from Yarn and NPM. Using the shell script is convenient for formatting multiple files at once or entire codebases10. However, this is impractical for development as it would require manual invocation every time someone wants to format their file. To make Prettier easier to use there are plugins for popular editors that can format the file you are writing with a command or on save11.
Prettier can also be integrated into existing workflows and code quality tools. For example, Prettier can be used as an ESlint rule12, a pre-commit hook13, or a Github Action to give further guarantees on consistency and code quality.
In all these execution environments the same program is executed. The plugins are there to make Prettier easier to use by porting it to tools that developers already use. However, these plugins are not directly related to code formatting and are not part of Prettier’s core. In subsequent sections we will dive deeper into the code formatting program, starting by looking at the different logical components that Prettier is made up of.
Components and connectors view
The core of Prettier is dependent on many libraries and node packages. The Prettier repository provides files that handle this automatically. The programming languages used to build these components are mainly JavaScript and TypeScript, but some other languages, such as CSS and HTML, are used as well. Prettier has separate code and plugins for different languages, therefore maintaining the overview of the functionalities of Prettier. Interaction between the components is supported by the underlying process execution infrastructure of the operating system. The details of the connectors will not be discussed as JavaScript solves the interaction by function calls.
Looking at the repository, and diving into the src-folder specifically, we can see that for each language there is a set of source files relevant for its transformation and styling. These language-specific files have no direct connection to the other languages’ code. This modular design makes sense since styling HTML is a lot different than styling JavaScript or TypeScript. For example, the JavaScript parsing folder has multiple different optional features depending on the situation. There are specific parser files for TypeScript and JSON for example14. Each subset of JavaScript has its own rules and parsing options, adding to the flexibility of Prettier and emphasizing the many options that this program offers. Then, after the code is parsed, it is transformed and printed in a more convenient and consistent style which Prettier aims to achieve, making the parsing, transformation, and printing functionalities the components of the program.
The components seen in the figure above are usually contained within dedicated source files containing all the code where the refactoring and styling happen15. These files contain a lot of relevant code related to the language of the target source file and contain various functions and rules that decide the best way to style the code. Some of the rules can be configured, but it has to be kept in mind that we are dealing with an opinionated code formatter here, which means some configurations may not always conform to everyone’s standards.
Development view
In the figure below we see a UML package diagram of the Prettier project. The folders in the diagram are packages or modules, the dotted arrows symbolize dependencies, and the triangles indicate that only those aspects of the system that are relevant for the diagram are included16. To make the view less cluttered, we have created the package language-* to represent the packages for the different languages.
The main packages of the Prettier repository are displayed in the diagram below. The src-package is the main folder of the project that contains the logic for the Prettier formatter. The scripts folder contains scripts for building, releasing, and handling the vendors-folder. Currently, Prettier is developed using CommonJS Modules and they intend to migrate to ECMAScript Modules17. While working on the migration, the Prettier repository contains the bundled files with Pure ESM Packages as CommonJS Modules in the vendors-package. The tests folder contains the test. Types contains files for handling types in Angular and Espree. Bin holds a single file for running Prettier from the shell. The node_modules are imported packages that Prettier depends on. An important part of the website folder is the playground-packages as this handles the Prettier Playground used for sharing discovered issues.
Due to the size of the Prettier repository, another Package diagram with more focus on the src-package is presented. To make the diagram less crowded we excluded the dependency arrows from the language-packages to some packages. The core of the Prettier algorithm is implemented in the document-package. The main-package is where the logic for the AST is kept. The common-package holds code shared in the src-package. Config and utils have code for handling config-files and useful extra functions respectively.
Runtime view
The runtime view describes the concrete behavior and interactions of the system’s building blocks18. Earlier the pipes and filters approach of Prettier was explained. A more detailed view of this process is presented below. The process is based on a formatting request from the CLI, but it is similar to using an IDE extension or a pre-commit hook. In the figure, the call path of the central files used for formatting is displayed, color-coded by package.
In the UML sequence diagram below the process in the figure above is displayed in more detail. The CLI commands are formatted and sent to the cli/index.js file. From the index.js file, the plugins such as language features are fetched. The core.js file calls the parser.js which sends the parsed text and the accompanying AST The AST is then sent to be converted to a document which is a series of printing primitives. The document and the AST are then formatted by the pretty print algorithm in document/index.js. The formatted files are returned to the user. If there is an error the process will return an exit code = 1, send an error message, and not format the file.
How the architecture realizes key quality attributes
In this section, we will explain how the architecture realizes the key quality attributes of Prettier, and how potential trade-offs between them are resolved.
Usability as an external quality attribute is implemented in multiple ways. First of all, Prettier is aimed to be as easy to use as possible. It can be used in different IDEs, in the shell, or in a CI/CD-pipeline. This is all meant to ensure that Prettier is as simple as possible to utilize for end-users, as mentioned in the section about the containers view. Furthermore, Prettier provides both node and yarn packages. This possibility of letting users use their favorite package manager is another way in which Prettier tries to be as simple as possible to use.
Modifiability is an important internal quality attribute since Prettier should be able to support new languages or new features of existing languages. Prettier enables developers to add new languages by using plugins19. It has four official plugins, and many more plugins developed by the community.
Consistency is another internal quality attribute that is important for the development of Prettier as a project. They have set up a very rigorous testing infrastructure, to ensure that all the contributions to the project follow the same formatting choices20 21. They use a test-driven development strategy, which translates into the fact that after running yarn test --coverage
Prettier has a test coverage well above 90%. From this high test coverage, we can see that Prettier values consistency highly.
We have not been able to find any significant trade-offs between the quality attributes. Of the three key quality attributes mentioned, both Modifiability and Consistency are internal quality attributes that help ensure the Usability of Prettier. Therefore, the three quality attributes help strengthen each other, and no trade-offs are needed.
API design principles
Prettier provides the option to run it programmatically using an API22. The most important design principles23 of this API are the Few Interfaces Principle, the Clear Interfaces Principle, and the Principle of Least Astonishment. In the following two paragraphs, we will explain why this is the case.
The design rationale behind Prettier’s API is based on their option philosophy24, which states that there should be as few options as possible, and modifiability with functions like formatWithCursor(source [, options])
for editor integrations and the Custom Parser API22. They provide a few options that are relevant to someone who wants to change parser or editor integration, but no more. This means that the API will be trimmed down and relatively simple to use for beginning users. Thus, two API design principles are important here: the Few Interfaces Principle (when in doubt leave it out23), and the Clear Interfaces Principle (maximize information hiding23).
As mentioned previously, consistency is an important quality attribute for Prettier. This can also be seen in the API design principles, where consistency, both internally and externally, is important. This is how the Principle of Least Astonishment - do not surprise users23 - is implemented into the API.
Sources
-
Prettier, Prettier documentation, retrieved march 9th, from https://prettier.io/docs/en/index.html ↩︎
-
Twitter. (2021, Jan. 13), vjeux ✪ on Twitter: “Prettier is a choice for the first time in the state of js survey and 60% of people said they were using it! This is insane! https://t.co/REW6L88voE https://t.co/XrrK3WQ5B8" / Twitter. Retrieved March 3, 2022, https://twitter.com/vjeux/status/1349383134010200068 ↩︎
-
Arie van Deursen. (2022, Feb. 11). Envisioning the System (E1, E2). TU Delft, https://se.ewi.tudelft.nl/delftswa/2022/. Retrieved February 11, 2022. ↩︎
-
Christopher Alexander et al..A Pattern Language (Oxford, 1977). Retrieved March 4 2022. ↩︎
-
Wikipedia. (2022, Feb. 02), Architectural pattern - Wikipedia. Retrieved March 4, 2022, https://en.wikipedia.org/wiki/Architectural_pattern. ↩︎
-
Wikipedia. (2021, Dec. 24), List of software architecture styles and patterns - Wikipedia. Retrieved March 4, 2022, https://en.wikipedia.org/wiki/List_of_software_architecture_styles_and_patterns. ↩︎
-
Wikipedia. (2021, Nov. 24), Pipeline (software) - Wikipedia. Retrieved March 4, 2022, https://en.wikipedia.org/wiki/Pipeline_(software). ↩︎
-
Wikipedia. (2021, Jul. 13), Multi-pass compiler - Wikipedia. Retrieved March 6, 2022, https://en.wikipedia.org/wiki/Multi-pass_compiler. ↩︎
-
Youtube. (2017, Mar 16), ⚡️ - James Long - A Prettier Printer (plus bonus clip!) - React Conf 2017 - Youtube. Retrieved February 26, 2022, https://www.youtube.com/watch?v=hkfBvpEfWdA ↩︎
-
Prettier. Install · Prettier. Retrieved March 6th, from https://prettier.io/docs/en/install.html. ↩︎
-
Prettier. Editor Integration · Prettier. Retrieved March 6th, from https://prettier.io/docs/en/editors.html. ↩︎
-
Prettier. Integration with Linters · Prettier. Retrieved March 6th, from https://prettier.io/docs/en/integrating-with-linters.html. ↩︎
-
Prettier. Pre-commit Hook · Prettier. Retrieved March 6th, from https://prettier.io/docs/en/precommit.html. ↩︎
-
Prettier, Prettier repo, retrieved March 10th, from https://github.com/prettier/prettier/tree/main/src/language-js ↩︎
-
Prettier, Prettier repo, retrieved march 9th, from https://github.com/prettier/prettier ↩︎
-
Fakhroutdinov, K. (2009). UML package diagrams graphical notation reference - package, stereotype, relationships between them: import, access, merge, etc. Uml-Diagrams. Retrieved March 7, 2022, from https://www.uml-diagrams.org/package-diagrams-reference.html ↩︎
-
Cheung, F. (2021, January 27). Drop support for Node.js 10, switch to ES Module · Issue #10157 · prettier/prettier. GitHub. Retrieved March 7, 2022, from https://github.com/prettier/prettier/issues/10157 ↩︎
-
Arc42. (2017). 6 - Runtime view - arc42 Documentation. Retrieved March 7, 2022, from https://docs.arc42.org/section-6/ ↩︎
-
Prettier. Plugins · Prettier. Retrieved March 12th, 2022, from https://prettier.io/docs/en/plugins.html ↩︎
-
Contributing to Prettier, Prettier repo, retrieved March 13th, from https://github.com/prettier/prettier/blob/main/CONTRIBUTING.md ↩︎
-
prettier/tests, Prettier repo, retrieved March 13th, from https://github.com/prettier/prettier/tree/main/tests ↩︎
-
Prettier, Prettier documentation, retrieved march 9th, from https://prettier.io/docs/en/api.html ↩︎
-
Arie van Deursen. (2022, Feb. 23). Views and Beyond (E2 cont.). TU Delft, https://se.ewi.tudelft.nl/delftswa/2022/. Retrieved March 14, 2022. ↩︎
-
Prettier. (2022). Option Philosophy. Retrieved March 12, 2022, from https://prettier.io/docs/en/option-philosophy.html ↩︎