Rome Toolchain

By Sebastian McKenzie at December 6, 2020

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.

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 Parsing Formatting Linting
JavaScript
— TypeScript
— JSX
JSON
RJSON
HTML #983
CSS #984
Markdown #985
yarn add rome
yarn rome init
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.

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!

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.

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

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.

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.

Rome has two different types of 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

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.

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.

See Project Configuration for configuration options.

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.

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.

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:

In JavaScript and CSS it can be a block comment:

And in Markdown and HTML:

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

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

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

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.

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

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

Adds some flavor to diagnostics.

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

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

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

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

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

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

Output additional information about diagnostics and disable truncation.

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

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.

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

Enables server logs and outputs them to the console.

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

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

Collect performance markers.

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.

Change the path that --profile will write to.

Defaults to Profile-TIMESTAMP.json.

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.

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.

Don’t include workers in the profile.

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.

Change the path that --rage will write to.

Defaults to Rage-TIMESTAMP.tgz.

See reviewing.

Output basic timing information on command completion.

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

Show the location of the cache directory.

Clear all artifacts from the cache directory.

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 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.

Reformat all files without applying any fixes.

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.

Set the key to true.

Set the key to false.

Set the key to a string value.

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

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

Show the config location that would be modified.

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

Flags

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.

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

This command will never complete.

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

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.

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

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.

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.

Revert the last command. Equivalent to rome recover apply <MOST_RECENT_STORE_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.

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

Print the directory where files are stored.

Clear the entire contents of the recovery store.

Equivalent to running rome stop and then rome start.

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

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

Stop a daemon, if one is running.

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.

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

rome --write-shell-completions bashrome --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 bashrome --log-shell-completions fish

rome --log-shell-completions zsh

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

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.

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

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

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]"]
}
}
}

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"

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"

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

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"

Path patterns that you want to ignore from linting.

rome config push lint.ignore "some-path"

Custom variables you want to declare as global.

rome config push lint.globals SomeGlobal

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

rome config enable lint.requireSuppressionExplanations

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

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.

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

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.

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.

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 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.

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:

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

*.js
App*Page.ts

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

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.

Say that you have the following directory structure:

babies/junipercats/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 (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 (affecting 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.

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

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

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

{ 

foo: "bar"


}

Regular double quoted strings can have newlines.

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

{
unquotedKey: true
}

Commas are not required to separate elements of an array:

[
1
2
3
]

or an object:

{
a: 1
b: 2
c: 3
}

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

Example

5_000

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.

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.

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.

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.

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.

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.

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

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.

  • 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.
  • 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.