Skip to main content

Rome

Unifying the frontend development toolchain

The Rome Toolchain

Rome is a linter, compiler, bundler, and more for JavaScript, TypeScript, JSON, HTML, Markdown, and CSS.

Rome is designed to replace Babel, ESLint, webpack, Prettier, Jest, and others.

Rome unifies functionality that has previously been separate tools. Building upon a shared base allows us to provide a cohesive experience for processing code, displaying errors, parallelizing work, caching, and configuration.

Rome has strong conventions and aims to have minimal configuration. Read more about our project philosophy.

Rome is written in TypeScript and runs on Node.js. Rome has zero dependencies, and has largely been written from scratch. See credits for more information.

Rome is maintained by a team of volunteers under an established governance model.

Rome is MIT licensed and moderated under the Contributor Covenant Code of Conduct.

Development Status

Rome is currently only supported as a linter for JavaScript and TypeScript. We are actively working on support for other languages.

Once our usage as a linter has matured we will work on releasing the other parts of Rome and expand beyond linting. Significant implementation already exist for most functionality.

We plan on covering the following areas:

  • Bundling
  • Compiling
  • Documentation Generation
  • Formatting
  • Linting
  • Minification
  • Testing
  • Type Checking
  • … and more

Language Support

Language Parsing Formatting Linting
JavaScript
— TypeScript
— JSX
JSON
RJSON
HTML #983
CSS #984
Markdown #985

Getting Started

Installation and Usage

Yarn:

yarn add rome
yarn rome init

npm/npx:

npx rome init

After running this command, Rome will:

  • Add itself to package.json as dependency if it wasn’t present, and run your package manager to install
  • Generate .config/rome.rjson that serves as your project config.

If you’re putting Rome into an already established project and you’d like to automatically apply formatting and fixes, you can use:

npx rome init --apply

Refer to Project Configuration for configuration options.

Note: The .rjson extension. RJSON is a superset of JSON that supports more-concise syntax and features such as comments.

Start Linting

You can now run Rome commands! Linting can be accessed via the command:

npx rome check

Continue to the next section to learn more about linting in Rome!

Linting

We’ve built Rome to be fantastic at displaying diagnostics. When we show you an error we want to give you all the information you need to understand why it appeared, how you can fix it, and how to avoid it in the future.

Rome stands out in the following ways:

Rich UI: Our diagnostic format allows us to show you rich information with formatting. This includes line wrapping in the terminal, syntax highlighting, lists, hyperlinks, and more.

Fixes: We provide fixes for many lint errors, which can be applied automatically. If there are multiple ways to fix something then we suggest multiple fixes that you can choose.

Reviewing: We offer an interactive CLI command to make this process even easier. It allows you to go through each diagnostic and perform actions on them such as inserting a suppression comment or applying a specific fix.

Editor: You can use an editor integration to bring the power of Rome into your editor. This includes lint errors as you type, automatic formatting when saved, and code actions to select specific fixes.

Safety: We save a copy of all files before we modify them and cache them. This cache can be managed with the rome recover command. You will always be able to revert when Rome modifies your code even without a commit history.

Command Usage

The rome check command is used to find problems in your project. This includes:

  • Dependency verification
  • Formatting
  • Linting
  • package.json validation

We plan on expanding this list to include other checks such as dead code detection, license verification, type checking, and more.

Running rome check with no arguments will include all files in your project:

rome check

You can limit this to specific files or directories with:

rome check App.js components/

Rerun automatically every time a file changes:

rome check --watch

Apply safe fixes and formatting:

rome check --apply

Apply only formatting:

rome check --format-only

Choose suggested fixes:

rome check --review

Rules

We have support for over 100 rules, including the most common rules needed working with TypeScript and React.

See the full list of rules.

All rules are enabled by default, and cannot be disabled. Suppressions can be used to hide specific lint errors.

Formatting

To use the Rome linter we require usage of the Rome formatter. We offer powerful fixes for most of our lint errors, which can only be done by taking control of code formatting.

Notable formatting choices include:

  • Indentation: Hard tabs. Improved accessibility over two-spaced tabs.
  • Double string quotes. Consistent quote style across all supported languages.

Applying Fixes

Rome has two different types of fixes:

Safe Fixes

For some lint errors, the fixes are unambigious and can be applied automatically. Diagnostics that are fixable are indicated with a label that appears in the header:

src/App.js:20:12 lint/js/doubleEquals  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

To apply safe fixes and formatting, add the --apply flag:

rome check --apply

Suggested Fixes

These are for scenarios where there could be multiple ways to fix an issue, or doing so automatically would be unsafe. We include suggestions on some diagnostics for possible fixes. These require an explicit action to apply and can be done via reviewing.

Reviewing

All diagnostics have different actions that can be performed. These include applying fix suggestions, adding a suppression comment, and more.

They require an explicit action to apply and can be chosen via the CLI with the --review flag on any command:

rome check --review

This displays each diagnostic and provides you with a list of actions that you can select using keyboard navigation.

Alternatively, these actions can be applied via a supported editor integration.

Configuration

See Project Configuration for configuration options.

Diagnostics

Diagnostics are what Rome calls errors. They are emitted absolutely everywhere Rome finds a problem. This includes CLI argument parsing, JSON normalization, module resolution, lint errors, and more.

Anatomy

Diagnostics consist of six main parts:

  • The header contains the filename, line, and column. They refer to the position that we believe is the main cause of a problem.
  • Followed is the message which contains a single-line summary of what we believe is wrong.
  • The code frame contains a snippet of the file referred in the header. This allows you to see what it’s referring to without having to jump into your editor and look it up.
  • Advice is freeform and appears at the end of a diagnostic. It can include additional messages, lists, other code frames, and more. It gives you more details about why you’re seeing the diagnostic, and how you might fix it.

 Filename:Line:Columnpages/UserLoginPage.js:8:8 Categorylint/jsx-a11y/altText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Message  × Provide alt text when using img, area, input type='image', and object
    elements.
Code Frame     6  return <span className="App">
     7      <header className="App-header">
   > 8        <img src={logo2} className="App-logo" />
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     9        <p>
    10          Edit <code>src/App.js</code> and save to reload.
Advice  i Meaningful alternative text on elements helps users relying on screen
    readers to understand content's purpose within a page.

Suppressions

Diagnostics can be suppressed with a rome-ignore comment. Comments must be followed by the diagnostic categories you want to suppress and a mandatory explanation.

In JavaScript this can be a line comment:

// rome-ignore lint/js/useCamelCase: match upstream library casing

In JavaScript and CSS it can be a block comment:

/* rome-ignore lint/js/useCamelCase: match upstream library casing  */

And in Markdown and HTML:

<!-- rome-ignore html/useClosingNonVoid: allow self-closing divs -->
Enforcement

If a suppression comment does not match suppress at least one diagnostic for every category listed then it will result in an error.

Multiple categories

You can suppress multiple categories by separating them with a space.

// rome-ignore lint/js/useCamelCase lint/js/noExplicitAny
Explanation

You must provide an additional explanation for the suppressed error by prefixing it with a colon:

// rome-ignore lint/js/noExplicitAny: explanation here

Editor Integration

Get the most out of Rome by integrating it with your editor. You will get diagnostics as you type, and saving will automatically format your files.

Rome implements the Language Server Protocol (LSP) supported by various editors. We have official extensions available for:

Once an editor extension has been installed, the version of Rome in your project will be automatically found and used. As we improve Rome and add new functionality any changes will automatically work with your editor!

We welcome contributions adding official extensions for other mainstream editors. See contributing for more information. LSP communication is done by the rome lsp command.

CLI

Global Flags

These are flags that can be added to any Rome command.

--cwd <dir>

Allows you to explicitly set the current working directory. Otherwise it is the shell cwd when executing the CLI.

--fieri

Adds some flavor to diagnostics.

--max-diagnostics <num>

Set the maximum amount of diagnostics that can be displayed before truncation. Defaults to 20.

--review

Open an interactive review mode for any diagnostics output from a command.

--show-all-diagnostics

Output all diagnostics, don’t limit to --max-diagnostics.

--silent

Don’t write anything to stdout. stderr will still be written to for errors. Equivalent to adding >/dev/null to a command.

--temporary-daemon

Spin up the server in a dedicated process. When the command has finished the server will exit. See Daemon for more information.

--watch

Some commands support a watch mode that will respond to file changes. See Commands for support.

--verbose-diagnostics

Output additional information about diagnostics and disable truncation.

Debugging Flags

These are flags that allow you to debug Rome. These are available in all builds and releases.

--benchmark

Run a command multiple times and output timing information. The amount of iterations defaults to 10 and can be customized with the --benchmark-iterations flag.

This is useful as it will benchmark the command after server initialization and can reuse cache from previous runs making it a realistic information for a user with a server enabled.

--benchmark-iterations <count>

The amount of iterations to perform when using the --benchmark flag. Defaults to 10.

--logs

Enables server logs and outputs them to the console.

--log-workers

Enables worker logs, by default these are not output when running --logs.

--log-path <path>

Instead of logging to the console, write logs to a specific file.

--markers-path <path>

Collect performance markers.

--profile

Start CPU profiling all processes and at the end of the command write a CPU profile to disk. Processes include the CLI, server, and workers.

This profile can be loaded into the Chrome Devtools Performance panel.

Upon command completion the profile will be written to the path specified by the --profile-path flag.

--profile-path <path>

Change the path that --profile will write to.

Defaults to Profile-TIMESTAMP.json.

--profile-sampling <microseconds>

A sampling CPU profiler, like the one in V8, works by polling on a set interval to track what code is being ran. This means that work which happens very quickly often times will not be captured in a profile.

You can customize this to reduce or increase the timing resolution. The lower the number, the larger but more accurate the profile. However, it may slow down.

Defaults to 200.

--profile-timeout <milliseconds>

Write the profile after the specified milliseconds have passed. This is useful for commands that take a long time to run and produce very large profiles.

--no-profile-workers

Don’t include workers in the profile.

--rage

Produces a rage archive. A rage archive is a .tar.gz file that contains information that is useful for debugging performance or bugs. It contains environment and command information, a CPU profile, and logs.

Upon command completion the archive will be written to the path specified by the --rage-path flag.

WARNING: Possible sensitive information such as path names and terminal environment will be visible. It’s recommended that you only share this privately with core contributors.

--rage-path <path>

Change the path that --rage will write to.

Defaults to Rage-TIMESTAMP.tgz.

--review

See reviewing.

--timing

Output basic timing information on command completion.

--watch

For commands that support it, rerun and update on file changes.

Commands

rome cache dir

Show the location of the cache directory.

rome cache clear

Clear all artifacts from the cache directory.

rome check

Used to find problems in your project. This includes:

  • Dependency verification
  • Formatting
  • Linting
  • package.json validation

See Linting: Command Usage for more usage information.

Flags

  • --apply

Apply formatting and safe fixes.

  • --changed <branch/commit>

Only include files that were changed between the specified branch/commit. This can be useful for performance in large projects.

If the branch/commit is omitted then we default to the default branch, either main or master. ie. rome check --changed is equivalent to rome check --changed main.

  • --format-only

Reformat all files without applying any fixes.

rome config

Used to modify project configuration. These commands work with all Rome project config locations (see supported locations for more info). When formatting a project config written with RJSON, comments will be retained.

Before your project config is saved, we will validate it for errors. It is not possible to save an invalid config with rome config.

Refer to Project Configuration: Properties for example commands.

rome config enable <key>

Set the key to true.

rome config disable <key>

Set the key to false.

rome config set <key> <value>

Set the key to a string value.

rome config set-directory <key> <value>

Set the key to the string value. If value is an absolute path then it will be made relative to the config path.

rome config push <key> <value>

Push the string value to an array at key. If key doesn’t exist then it will be created.

rome config location

Show the config location that would be modified.

rome init

This command assists in the creation of a new Rome project. Actions that are performed:

Flags

  • --apply

Additional operations are applied with this flag:

  • rome check --apply is ran which will automatically format and autofix your files.
  • Global variables are extracted from previous errors and automatically added to your project config.

Uncomitted changes and --apply

Since this command can be destructive and may have unintended consequences, we check if you have any uncomitted changes. It’s important to make sure you have everything committed in case you aren’t happy with the effects of running this command. ie. you run into a bug, you don’t like Rome, or want to try it some other time. You can bypass this restriction by adding the --allow-dirty flag.

rome logs

Alias for rome noop --logs --hang. See --logs documentation for more info.

This command will never complete.

rome lsp

Running this command will start a long-running server and communicate via the Language Server Protocol over stdio. This command takes no flags.

rome noop

This command does nothing. Used in conjunction with other global flags such as --logs and --rage.

Flags

  • --hang Instead of immediately exiting, hang the command and never exit unless forced.

rome rage

Alias for rome noop --rage. See --rage documentation for more info.

rome recover

Whenever Rome needs to write files to the disk, for example when updating the formatting or autofixing a file, we first save a copy of the original file to an internal cache that we call the “recovery store”. This is to allow you to revert your changes if necessary. This command is used to interact with this store.

We only keep the content of the last 5 commands that modified files. After that we will delete the oldest entry.

rome recover list

Show the contents of the recovery store. Including the command that was ran, at what time, files that were changed, and the recover commands you can use to perform operations.

rome recover pop

Revert the last command. Equivalent to rome recover apply <MOST_RECENT_STORE_ID>.

rome recover apply <id>

Revert the changes that were made by the corresponding id. You can find the id by running rome recover list.

Running this command will also produce a new store entry with the files that were reverted.

rome recover diff <id>

Produce a diff of changes between existing files and those included in the id store.

rome recover dir

Print the directory where files are stored.

rome recover clear

Clear the entire contents of the recovery store.

rome restart

Equivalent to running rome stop and then rome start.

rome start

Start a daemon, if one isn’t already running.

rome status

Output the status of a running daemon. This includes uptime, file count, and other useful scaling statistics.

rome stop

Stop a daemon, if one is running.

Daemon

Rome has a server architecture that’s designed to run well as a long-running process, maintaining memory caches and automatically responding to file changes.

This behavior is however optional. By default, when running the CLI, we do not create a daemon. However, if there is a daemon available then we will connect to it.

You can explicitly start a daemon with the rome start command and control it with rome restart, rome status, and rome stop.

Shell Completions

Completions commands are available for bash, fish and zsh. To automatically install them run:

rome --write-shell-completions bash
rome --write-shell-completions fish
rome --write-shell-completions zsh

This will automatically write the completions to a file and add it to your shell profile if necessary.

NOTE: This file is static. You may need to run this command whenever Rome is updated for up-to-date completions.

Alternatively you can run:

rome --log-shell-completions bash
rome --log-shell-completions fish
rome --log-shell-completions zsh

which instead will output the completions to stdout rather than a file.

bash

We will write the completions to ~/.rome/rome-completions.sh. We will add this file as a source to either ~/.bashrc or ~/.bash_profile, whatever we can find first.

fish

We will write the completions to ~/.config/fish/completions/rome.fish. No profile modification is necessary as they are automatically loaded.

zsh

We will write the completions to ~/.zsh-completions/_rome.

Project Configuration

Rome needs to know how to find your project and what files it includes. To do this we require a project configuration file.

Your configuration can be placed in a few different locations, but we recommend using a single rome.rjson file. This file is written using RJSON which is our flavor of JSON. It supports comments and has a simpler syntax.

All properties are optional, you can even have an empty config! We recommend using the rome config command to modify your configuration, this works with any of the supported config locations, and when editing RJSON will even retain comments.

We are deliberately lean with the supported configuration. We do not include options just for the sake of personalization. We aim to offer everything out of the box and only introduce configuration if absolutely necessary.

name: "project-name"
version: "^0.0.0"
root: true
extends: "../other-file"

lint: {
ignore: []
globals: []
}
dependencies: {
exceptions: {
invalidLicenses: {
"funky-licence": ["[email protected]", "[email protected]", "[email protected]"]
}
}
}

Properties

name

This is your project name. It is typically whatever you have set as name in package.json. This is never shown to you, and is used internally to refer to your project.

The Rome cache is portable, meaning it contains no references to absolute paths. This allows it to be stored across different machines. This feature may not be important to you so it can be safely omitted in most cases.

rome config set name "project-name"

extends

Inherit from another file and merge configuration. If you would only like to share partial configuration then extract it into a separate config that is used instead.

If the file refers to a package.json file then the rome property is used.

rome config set-directory extends "some-other-file"

root

By default, Rome supports nested projects and will search parent directories for other projects to initialize. Sometimes this isn’t what you want and can cause unexpected problems. This can be solved by explicitly setting the root flag which tells Rome that it should ignore any parent directories.

rome config enable root
rome config disable root

version

This is a semver range of the Rome version you want to set your project to. It is an optional layer of protection and can avoid version mismatches in large monorepos and projects.

rome config set version "^0.0.0"

lint.ignore

Path patterns that you want to ignore from linting.

rome config push lint.ignore "some-path"

lint.globals

Custom variables you want to declare as global.

rome config push lint.globals SomeGlobal

lint.requireSuppressionExplanations

Raise a diagnostic if a suppression does not have a valid explanation.

rome config enable lint.requireSuppressionExplanations

dependencies.exceptions

Exception rules for your dependencies that don’t pass validation.

dependencies.exceptions.invalidLicenses

Sometimes Rome might complain that one or more of your dependencies has an invalid license.

Optionally, you can insert the name of this invalid license here:

rome config push dependencies.exceptions.invalidLicenses.invalid-license-name "[email protected]"

If you are unsure about the license name of your library, rome will suggest the command for you when you try to run a command.

Supported Locations

You can specify your project config in a few different places.

.config/rome.rjson (recommended)

This is the recommend location. It’s the file we create when running rome init.

It can contains Rome’s flavor of JSON, RJSON, that allows comments and simpler syntax.

.config/rome.json

You can also use rome.json with regular JSON. This is useful if you think you might want to process and manipulate project configuration with another tool or language.

package.json field

Alternatively, your project config can be included in a rome field inside of package.json:

{
"name": "my-package",
"version": "0.0.0",
"rome": {
"version": "^0.0.1"
}
}

Nested Projects

Nested projects are a first-class feature and can be used to customize configuration for different parts of your codebase. Multiple projects can be loaded inside of a single Rome process.

When running any command or operation on a file, we refer to the project it is a part of when considering any configuration rather than what directory it was ran from.

Path Patterns

Some configuration options contain path patterns. If you have ever used .gitignore then it’s the same familiar syntax. These are also called glob patterns.

These are paths that contain special characters for matching files and directories. These are typically used for ignore rules.

We support the following syntax:

Wildcards

* matches any number of any characters including none in a directory. This can be used in any filename position. ie.

*.js
App*Page.ts
Matching Directories

A pattern that matches a directory will also match every file inside of it. eg. pages is the same as writing pages/**/*.

Negations

Sometimes you want to add exceptions to a rule. For example, you have a folder you want to ignore but there is a file inside of that you don’t want to match. You can do this by prefixing it with !. For example:

scripts
!scripts/navigation.js

This will ignore everything in the scripts directory besides the file navigation.js.

Base Directory

Say that you have the following directory structure:

babies/juniper
cats/babies/orion
cats/babies/nev

And you only wanted to ignore the folder babies that contains juniper. If you wrote just babies then it would match both directories. However, if you prefix it with a black slash, as in /babies, then it will only match the folder at the base.

Rome JSON

Rome JSON (RJSON) is a superset of JSON. It does not add any new data types. It just makes some syntax optional for the sake of readability.

We wanted to allow comments inside Rome configuration files. Existing JSON supersets either add new data types (effecting portability), introduce syntax variants, or offer no way to edit the JSON and retain the original comments. This necessitated the creation of our own JSON parser.

RJSON is a superset, meaning that it is backwards compatible and accepts all existing JSON. All places where RJSON files are allowed, you can alternatively use a regular JSON file where these syntax extensions wont be allowed.

Implicit top level object

You can omit the curly braces for a top-level object and we will treat it as an object.

foo: "bar"
"bar": "foo"
Comments

Standard JavaScript comments are supported. Both line and block comments.

{
// Line comment
/* Block comment */
foo: "bar"
}
Multiline strings

Regular double quoted strings can have newlines.

Unquoted keys

If a property key is a valid identifier then the quotes can be omitted, just like in regular JavaScript.

{
unquotedKey: true
}
Optional commas

Commas are not required to separate elements of an array:

[
1
2
3
]

or an object:

{
a: 1
b: 2
c: 3
}
Numeric separators

You can use numeric separators in numbers, just like in regular JavaScript:

Example

5_000

Internals

Process Model

Rome consists of three process types:

  • Client. eg. CLI. Responsible for building a request and dispatching it to a server. If there is a running daemon then it’s used as the server, otherwise, the CLI creates a server inside of its process, which only the lifetime of the request.
  • Server. Where the magic happens. Watches the file system and maintains an in-memory copy for fast lookups. Spawns workers and distributes and coordinates work between them. Responds to requests from the CLI.
  • Worker. Where distributed work occurs. These are operations that are specific to a certain file. Produces artifacts that can be easily cached. Computed values contain enough information to aggregate with other file operations to provide cross-file analysis.

Immutable AST

All parsed ASTs are treated as immutable. This allows reference equality to be used to quickly determine if a node has been modified and can be used as keys in a WeakMap for effective memory caching.

Recoverable Parsers

All parsers are recoverable, always deriving an AST despite syntax errors. This allows operations that validate code to be chained together. This surfaces as many problems as possible at once and reduces the waterfall of fixing errors only to be faced with more.

Portable Cache

Internally we use unique IDs to refer to files rather than absolute paths. This allows cache artifacts to be transferred between different machines. Included are hashes of the project config, mtime, and other file information to allow for easy validation.

This can be utilized in a CI environment, or even in a network cache for a group of developers. We will add the relevant hooks in the future to allow this to be used more effectively, including a dedicated network cache server.

Terminal Rendering

We have our own HTML-ish markup format that is used to declare formatting in strings. We use this format everywhere rather than traditional embedded ANSI color codes. This allows us to remain output agnostic. We currently support rendering to ANSI, HTML, and plain text.

All the “terminal screenshots” you see in the docs were generated from regular Rome CLI commands with the --output-format html --output-columns 80 flags set.

Tags are not color-specific. ie. rather than <green> we have <success>. This makes our markup even more semantic and versatile.

When rendering we perform layout calculation according to a provided column width, in most cases reported to us by the shell. This layout calculation includes line wrapping, padding, horizontal rules, and text alignment.

We avoid the common pitfalls of in-band ANSI formatting by doing the formatting as the final step when all the text has been split into non-overlapping ranges for ANSI code insertion.

Type Safety

While we are in JavaScript land, we embrace TypeScript by using as many strong types as possible. We have sparing usages of wide types like object and any casts. With no dependencies we are able to extend this coverage and confidence everywhere. We never consume arbitrary data like JSON without first passing it through some validation and normalization process.

Self Hosted

Rome is bundled, compiled, linted, and tested by itself. Once Rome was built and had the capabilities necessary to build itself, we removed the other tools and instead used a build of Rome.

Read more about self hosting at Self-hosting (compilers) - Wikipedia

Philosophy

This list includes general ethos the project should abide by. This list is not comprehensive. Some of these are obvious but are stated for completeness.

Project Management

  • Set clear expectations. Make project intent and decisions known well in advance. Nothing should be a surprise.
  • Transparency. No back-channel project management. Project conversation and decisions will take place only on public forums such as GitHub, the Rome Discord, and Twitter. The only exception to this is moderation decisions which will be strictly done in private.

Technical

  • No external dependencies. This allows us to develop faster and provide a more cohesive experience by integrating internal libraries more tightly and sharing concepts and abstractions. There always exist opportunities to have a better experience by having something purpose-built.
  • Errors should suggest fixes and hints where possible. These should be inferred and filtered from usage to reduce surfacing irrelevant and unhelpful messages.
  • Unique and specific error messages. No generic error messages. This not only helps users understand what went wrong, but should provide maintainers with a unique call site and the necessary information to debug.
  • Minimize API. Question the existence of all options and flags. Are they necessary? Can they be combined? How can we reduce code branching?
  • Reduce jargon. Don’t assume that users will understand specific terminology. Strive to provide clear meaning for experts and beginners. For example, use “character” where you would traditionally use “token” when producing parser errors.
  • Utilize verbosity when naming commands and flags. No unnecessary and confusing abbreviations.
  • Use inclusive terminology. Use gender-neutral pronouns. No ableist slurs. No usage of terms that could be considered insensitive.
  • Build for generic clients. Don’t assume that output will only be consumed by a terminal and using ANSI codes. Use abstractions that could be generalized for viewing in an IDE, browser, or other environments.
  • Use strong types. Don’t use loose types such as any. Where possible, refine and validate input. Aim for sound types.
  • Terminal output should be unambiguous. When designing terminal output, don’t purely rely on formatting cues such as color. Always use a combination of formatting, symbols, and spacing. If all ANSI codes are stripped, all the output should still be understood.