# Overview

Rome is a toolchain where users can use only Rome to manage their code. On the other hand, users should be free to use only specific tools in their workflow, without too much hassle.

While the latter is desirable, using Rome as all-in-one tool is preferred on the long run because it creates a homogeneous user experience in terms of:
- performance
- error messaging
- caching
- overall workflow between tools
- good defaults

Considering what written above, the Node.js API should reflect what stated above.

## Terminology

From now on the document will use some terms, here's their context inside the document:

- **Frontend**: the code written for the runtimes (Node.js, Deno, browser, etc.);
- **Rust Workspace**: the main hub where the bulk of the logic resides. It's the Rust code that live
  inside the `rome_service` crate;
- **Runtime Workspace**: it's the shared code used by the different Frontends;


## Use case for Rome

The first and foremost important tool that users should access to is Rome's compiler. The compiler,
as this time, it's not ready yet - at least not all the features we intend to implement,
but this was a design decision when it was first created.

Exposing the compiler should allow a user to do multiple things (not limited to this list):
- allow to transform some code (transplilation, minification, compile to another language, etc.)
- analyze some code (linter, control flow analysis, type check, etc.)
- bundle some code
- generate documentation

The end goal of how Rome can expose its compiler is via plugins. **Plugins are a non-goal for this
RFC**, but this first proposal should start taking them into consideration.

As for today, Rome can expose a formatter and a linter (analyzer).

## The aim of the APIs

The APIs should only be a **medium** to eventually communicate with the Rust Workspace,
which means that the APIs can't provide more features than what the Rust Workspace can provide.

While designing the APIs, it's important to take this in consideration.


# The current ecosystem

As for today, different tools have different approaches

## `prettier`

Their APIs are not aligned with the CLI. The CLI allows to format a single file, a list of files or
some code passed via `stdin`. The APIs mostly orbit around formatting some content, and they are not
aware if prettier is installed already. This works well for most environments but the majority of the
work is responsibility of the user:
- read files and their contents
- resolve the configuration in case prettier is already installed in the project

## `esbuild`

Their APIs and CLI are almost aligned, meaning that the options passed via APIs are mapped 100% to the
CLI, for example:
```js
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
  platform: 'node',
  external: ['fsevents'],
})
```

```shell
esbuild app.js --bundle --external:fsevents --platform=node
```

> It seems to me that esbuild is not designed to be a standalone dev server, then caching strategies
> and whatnot are designed for this goal. I might be wrong.

`esbuild` is mostly designed to be a bundler, so their APIs and design decisions might orbit
around that.

## `eslint`

`eslint` takes a different approach compared to `prettier`. Their approach emulates their CLI but
with extra steps:
```js
// 1. Create an instance.
const eslint = new ESLint();

// 2. Lint files.
const results = await eslint.lintFiles(["lib/**/*.js"]);

// 3. Format the results.
const formatter = await eslint.loadFormatter("stylish");
const resultText = formatter.format(results);

// 4. Output it.
console.log(resultText);
```

They use the instance pattern. The instance accept a [bunch of options](https://eslint.org/docs/latest/developer-guide/nodejs-api#parameters).

```shell
eslint lib/**/*.js
```

There's also one more thing to note, their Node.js API is designed mostly for plugin authors. From
their website:
> The purpose of the Node.js API is to allow plugin and tool authors to use the ESLint functionality directly, without going through the command line interface.

`eslint` also allows to lint some content:
```js
const results = await eslint.lintText(code, options);
```

## `postcss`

They use the instance pattern:

```js
let postcss = require('postcss')

postcss(plugins).process(css, { from, to }).then(result => {
  console.log(result.css)
})
```

```shell
postcss input.css -o output.css
postcss src/**/*.css --base src --dir build
cat input.css | postcss -u autoprefixer > output.css
```

Even though the approach is slightly different from `eslint`, the result is the same.

# Rome's APIs

Here's a list possible proposals of how Rome could be used.

There might be various cases, but first we need to understand how Rome is designed, and make proposal
based its design **AND** end-goal.

![Runtime design](./Runtime%20design.png)


1. Rome is configuration aware, meaning that when communicating with Rust Workspace,
   all configuration defaults are automatically applied. If the APIs are run
   inside a Rome project, the configuration is automatically picked up from the `rome.json` file.
2. Rome's APIs should reflect the CLIs commands and arguments. What it's possible to do
   via CLI, should be done also via APIs, but not vice-versa. This constraint would allow the team
   to first design and test the feature natively, and expose it once it's stable.
3. Rome should be able, in the future, to expose plugins or a way to users to use its compiler capabilities

> Point `2.` might not be true for ALL cases. For example, as for today, we can't format by
> range via CLI. Maybe this should be more of a guideline, and not a constraint.



## Instance pattern

Instance pattern

```js
import { Rome } from "rome";

let rome = new Rome({});
```

The instance pattern allows for better extensions on the long run, and allows to play better with
all the tools we want to expose.

```js
import { Rome } from "rome";

const content = "function f() { return {} }";
const rome = new Rome({
    formatter: {
        lineWidth: 120
    },
    linter: {
        recommended: false,
        js: {
            noDeadCode: "error"
        }
    }
});
const ast = rome.parseContent(content); // not part of this paragraph
const new_content = rome.formatContent(content); // not part of this paragraph
const diagnostics = rome.checkContent(content);  // not part of this paragraph
```

The first parameter of the instance should be a personalized configuration. The configuration
passed to the instance **will override** Rome's defaults  **_BUT_** not the options inside a possible
`rome.json` file.

This is an import point, because we might have cases where a user is using Rome for linting/formatting
of the project, but this project is actually using the runtime API to do some ad-hoc work. For example
scripts, some generated code, etc.

In the future the instance pattern will be useful to register plugins. Plugin need to have access
to the compiler APIs (we don't know yet what kind of APIs and what information).


## Formatter

While using functions à la `prettier` may seem quicker for prototyping, this approach might
create more friction in the future when we will extend the APIs with new features.

### Formatting a file

```js
import { Rome } from "rome";

const rome = new Rome();
const result = rome.formatFiles(["./path/to/file.js"]);
console.log(result.code); // formatted content
console.log(result.errors); // possible parse errors
```

Which reflects

```shell
rome format ./path/to/file.js
```

We can also write directly the new content to a file

```js
import { Rome } from "rome";

const rome = new Rome();
const result = rome.formatFile("./path/to/file.js", { write: true }); // `true` maps to the `--write` argument
console.log(result.code); // undefined, the new content is in the file
console.log(result.errors); // possible parse errors
```

Which reflects

```shell
rome format --write ./path/to/file.js
```

### Formatting a directory

It should be possible to format a directory

```js
import { Rome } from "rome";

const rome = new Rome();
const result = rome.formatFiles(["./path"]);
for (const [file_name, result] of result) {
    console.log(file_name);
    console.log(result.code);
    console.log(result.errors); // errors thrown while formatting this file
}
```

Which reflects

```shell
rome format ./path
```

We can also write directly the new content to files


```js
import { Rome } from "rome";

const rome = new Rome();
const result = rome.formatFiles(["./path"], { write: true });
for (const [file_name, result] of result) {
    console.log(file_name);
    console.log(result.code); // undefined, it's being written
    console.log(result.errors); // errors thrown while formatting this file
}
```

> **Note**: `formatFile` could be removed in favour of only `formatFiles`

### Formatting some content

> **Warning**: as for today the CLI doesn't allow to format from `stdin`, but it's something
> that we plan to deliver ASAP

```js
import { Rome } from "rome";

const rome = new Rome();
const content = "function f()  { return   {}}";
const result = rome.formatContent(content, { filePath: "example.js" });
console.log(result.code); // formatted content
console.log(result.errors); // possible parse errors
```

Which will translate to

```shell
echo "function f()  { return   {}}" | rome format --file-type=js
```

`filePath` is required to tell Rome how it should parse the file.

### Format range

```js
import { Rome } from "rome";

const rome = new Rome();
const content = "function f()  { return   {}}";
const result = rome.formatContent(content, { filePath: "example.js", range: [7, 10] });
console.log(result.code); // formatted content
console.log(result.errors); // possible parse errors
```

> **Note**: the CLI doesn't allow to format ranges as for today, and it might not be needed.
> It can be an exception to the rule where each API should be supported by the CLI too.

### Debugging


As you noticed each call accepts an object as second argument. This object could contain a
`debug` property, which allows us to return the IR emitted by Rome.

```js
import { Rome } from "rome";

const rome = new Rome();
const result = rome.formatFiles(["./path/to/file.js"], { debug: true });
console.log(result.code); // formatted content
console.log(result.errors); // possible parse errors
console.log(result.ir); // the IR emitted by Rome
const content = "function f()  { return   {}}";
const result2 = rome.formatContent(content, { filePath: "example.js", range: [7, 10], debug: true });
console.log(result2.ir); // the IR emitted by the call formatContent
```

## Linter

> **Note**: nowadays, Rome doesn't have a CLI command to _only lint_ files. We have the `check`
> command that does that *now*, but the command is designed to run multiple checks.

It's also possible to run the linter and retrieve possible diagnostics

### Lint files

```js
import { Rome } from "rome";

const rome = new Rome();
const result = rome.lintFiles(["./path/to/file.js"]);
console.log(result.errors); // diagnostics emitted while lint the files
```


### Lint content

```js
import { Rome } from "rome";

const rome = new Rome();
const content = "function f()  { return   {}}";
const result = rome.lintContent(content, { filePath: "example.js" });
console.log(result.errors); // diagnostics emitted while lint the files
```

## Parse

Allows to parse a file, and return the CST and AST emitted by the parsing phase.

> **Note**: nowadays, the CLI doesn't a command to parse files, this could be perfect occasion
> to actually implement it.


> **Note**: The reason why `cst` and `ast` are emitted as strings is mostly because we don't have TypeScript file types
for our nodes. If we are able to generate TypeScript types for our AST, then `ast` could be
> returned as an object

### Parse content

```js
import { Rome } from "rome";

const rome = new Rome();
const content = "function f()  { return   {}}";
const result = rome.parseContent(content, { filePath: "example.js" });
console.log(result.ast); // AST as string
console.log(result.cst); // CST as string
console.log(result.errors); // possible parse errors
```


## Errors

This proposal assumes that **all** APIs are prone to errors for different reasons. Internally, in the
Rust Workspace, we emit a `RomeError`, which might contain other variants with other errors,
form example `ConfigurationError`.

At the moment this `RomeError` is not serialized, so it's not very easy to come up with a clear
proposal, but a runtime should receive at least a `code` of the error, e.g. `RomeError:ConfigurationError`,
maybe a `sub_code` e.g. `ConfigurationError::ConfigAlreadyExists`,


## Q&A

> Why not expose also functions like `format`, `lint`, etc.?

While this can be an option, and it could be done, I think they don't exactly fit in the grand
scheme of what Rome should be.

Of course there are exceptions, for example for a future testing framework:

```js
import {test} from "rome";

test("test something", t => {
    t.assert(true)
})
```

>