Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prettier Plugin API #3536

Merged
merged 42 commits into from Dec 26, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c4ed408
Move files around in preparation for refactor
azz Dec 19, 2017
a759a1c
Update paths in build script
azz Dec 20, 2017
568baef
Extract generic printing logic from the JavaScript printer
azz Dec 20, 2017
ddd8674
Conform printer API
azz Dec 20, 2017
f6b82ce
Fixup decorator handling
azz Dec 20, 2017
a482bb1
Fix multiparser
azz Dec 20, 2017
a6dc181
Create plugin entry for markdown
azz Dec 21, 2017
33afcd3
Create plugin entry for javascript/typescript
azz Dec 21, 2017
25100f9
Create plugin entry for html
azz Dec 21, 2017
704fa34
Create plugin entry for graphql
azz Dec 21, 2017
2679f3b
Create plugin entry for css/less/scss
azz Dec 21, 2017
7c6cc7b
Move JSON to JS plugin entry
azz Dec 21, 2017
1298d13
Integrate plugins into getSupportInfo()
azz Dec 21, 2017
db5fe5a
Move astFormat to parser definition
azz Dec 21, 2017
a8505d7
Move util to common
azz Dec 21, 2017
f3d3474
Implement parser loading
azz Dec 22, 2017
6a53cc0
remark -> mdast
azz Dec 23, 2017
a6b44d3
Rename cli/cli -> cli/index
azz Dec 23, 2017
20272af
Rename builder -> doc package, fix printer resolution
azz Dec 23, 2017
bd96a54
Fix doc shape assumption in CSS-in-JS logic
azz Dec 23, 2017
91c297f
Fix third-party.js prod resolution
azz Dec 23, 2017
4e103a1
Fixup build-docs script
azz Dec 23, 2017
48969e3
Distribute multiparser code
azz Dec 23, 2017
411484f
Remove requirement to forward options
azz Dec 23, 2017
f211d10
Flatten closure
azz Dec 23, 2017
d7f8707
Remove debug directory
azz Dec 23, 2017
0c97f15
Expose doc
azz Dec 24, 2017
df242c0
Add external plugins
azz Dec 24, 2017
870b9d8
Pass options to loadPlugins
azz Dec 24, 2017
9f105ac
Export getParsers
azz Dec 24, 2017
9676761
Pin resolve version
azz Dec 24, 2017
4f55e7d
Use getSupportInfo in Markdown embed
azz Dec 24, 2017
858023b
Document plugin API
azz Dec 24, 2017
c8b0bfb
Update build-docs
azz Dec 24, 2017
e46f31f
Add CLI for plugins
azz Dec 24, 2017
3a4f21a
Lint docs
azz Dec 24, 2017
68c83a6
Fixup build.js
azz Dec 24, 2017
289a46c
Add vue language
azz Dec 25, 2017
5a10c03
Fixup multiparser for vue
azz Dec 25, 2017
d81b7e6
Upgrade rollup and rollup-plugin-commonjs
azz Dec 25, 2017
8382c48
Fixup third-party build
azz Dec 25, 2017
52072e8
Change AST format in docs
azz Dec 25, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
155 changes: 155 additions & 0 deletions docs/plugins.md
@@ -0,0 +1,155 @@
---
id: plugins
title: Plugins
---

# IN DEVELOPMENT

> The plugin API is unreleased and the API may change!

Plugins are ways of adding new languages to Prettier. Prettier's own implementations of all languages are expressed using the plugin API. The core `prettier` package contains JavaScript and other web-focussed languages built in. For additional languages you'll need to install a plugin.

## Using Plugins

There are three ways to add plugins to Prettier:

* Via the CLI.
* Via the API.
* With a configuration file.

### Configuration File (Recommended)

In your [configuration file](./configuration.md), add the `plugins` property:

```json
{
"plugins": ["prettier-python"]
}
```

### CLI

With the [CLI](./cli.md), pass the `--plugin` flag:

```bash
prettier --write main.py --plugin prettier-python
```

> Tip: You can pass multiple `--plugin` flags.

## Official Plugins

* [`prettier-python`](https://github.com/prettier/prettier-python)
* [`prettier-php`](https://github.com/prettier/prettier-php)

## Developing Plugins

Prettier plugins are regular JavaScript modules with three exports, `languages`, `parsers` and `printers`.

### `languages`

Languages is an array of language definitions that your plugin will contribute to Prettier. It can include all of the fields specified in [`prettier.getSupportInfo()`](./api.md#prettiergetsupportinfo-version).

It **must** include `name` and `parsers`.

```js
export const languages = [
{
// The language name
name: "InterpretedDanceScript",
// Parsers that can parse this language.
// This can be built-in parsers, or parsers you have contributed via this plugin.
parsers: ["dance-parse"]
}
];
```

### `parsers`

Parsers convert code as a string into an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree).

The key must match the name in the `parsers` array from `languages`. The value contains a parse function and an AST format name.

```js
export const parsers = {
"dance-parse": {
parse,
// The name of the AST that
astFormat: "dance-ast"
}
};
```

The signature of the `parse` function is:

```ts
function parse(text: string, parsers: object, options: object): AST;
```

### `printers`

Printers convert ASTs into a Prettier intermediate representation, also known as a Doc.

The key must match the `astFormat` that the parser produces. The value contains an object with a `print` function and (optionally) an `embed` function.

```js
export const printers = {
"dance-ast": {
print,
embed
}
};
```

Printing is a recursive process of coverting an AST node (represented by a path to that node) into a doc. The doc is constructed using the [builder commands](https://github.com/prettier/prettier/blob/master/commands.md):

```js
const { concat, join, line, ifBreak, group } = require("prettier").doc.builders;
```

The signature of the `print` function is:

```ts
function print(
// Path to the AST node to print
path: FastPath,
options: object,
// Recursively print a child node
print: (path: FastPath) => Doc
): Doc;
```

Check out [prettier-python's printer](https://github.com/prettier/prettier-python/blob/034ba8a9551f3fa22cead41b323be0b28d06d13b/src/printer.js#L174) as an example.

Embedding refers to printing one language inside another. Examples of this are CSS-in-JS and Markdown code blocks. Plugins can switch to alternate languages using the `embed` function. Its signature is:

```ts
function embed(
// Path to the current AST node
path: FastPath,
// Print a node with the current printer
print: (path: FastPath) => Doc,
// Parse and print some text using a different parser.
// You should set `options.parser` to specify which parser to use.
textToDoc: (text: string, options: object) => Doc,
// Current options
options: object
): Doc | null;
```

If you don't want to switch to a different parser, simply return `null` or `undefined`.

## Testing Plugins

Since plugins can be resolved using relative paths, when working on one you can do:

```js
const prettier = require("prettier");
const code = "(add 1 2)";
prettier.format(code, {
parser: "lisp",
plugins: ["."]
});
```

This will resolve a plugin relative to the current working direcrory.
21 changes: 12 additions & 9 deletions index.js
@@ -1,15 +1,16 @@
"use strict";

const comments = require("./src/comments");
const comments = require("./src/main/comments");
const version = require("./package.json").version;
const printAstToDoc = require("./src/printer").printAstToDoc;
const util = require("./src/util");
const printDocToString = require("./src/doc-printer").printDocToString;
const normalizeOptions = require("./src/options").normalize;
const parser = require("./src/parser");
const printDocToDebug = require("./src/doc-debug").printDocToDebug;
const config = require("./src/resolve-config");
const getSupportInfo = require("./src/support").getSupportInfo;
const printAstToDoc = require("./src/main/ast-to-doc");
const util = require("./src/common/util");
const doc = require("./src/doc");
const printDocToString = doc.printer.printDocToString;
const printDocToDebug = doc.debug.printDocToDebug;
const normalizeOptions = require("./src/common/options").normalize;
const parser = require("./src/main/parser");
const config = require("./src/config/resolve-config");
const getSupportInfo = require("./src/common/support").getSupportInfo;
const docblock = require("jest-docblock");

function guessLineEnding(text) {
Expand Down Expand Up @@ -385,6 +386,8 @@ module.exports = {
}
},

doc,

resolveConfig: config.resolveConfig,
clearConfigCache: config.clearCache,

Expand Down
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -47,6 +47,7 @@
"postcss-values-parser": "1.3.1",
"remark-frontmatter": "1.1.0",
"remark-parse": "4.0.0",
"resolve": "1.5.0",
"semver": "5.4.1",
"string-width": "2.1.1",
"typescript": "2.6.2",
Expand All @@ -70,8 +71,8 @@
"prettier": "1.9.2",
"prettylint": "1.0.0",
"rimraf": "2.6.2",
"rollup": "0.41.6",
"rollup-plugin-commonjs": "7.0.2",
"rollup": "0.47.6",
"rollup-plugin-commonjs": "8.2.6",
"rollup-plugin-json": "2.1.1",
"rollup-plugin-node-builtins": "2.0.0",
"rollup-plugin-node-globals": "1.1.0",
Expand Down
23 changes: 8 additions & 15 deletions scripts/build/build-docs.js
Expand Up @@ -4,19 +4,12 @@

const path = require("path");
const shell = require("shelljs");
const parsers = require("./parsers");

const rootDir = path.join(__dirname, "..", "..");
const docs = path.join(rootDir, "website/static/lib");
const parsers = [
"babylon",
"flow",
"typescript",
"graphql",
"postcss",
"parse5",
"markdown",
"vue"
];

const stripLanguageDirectory = parserPath => parserPath.replace(/.*\//, "");

function pipe(string) {
return new shell.ShellString(string);
Expand All @@ -25,6 +18,8 @@ function pipe(string) {
const isPullRequest = process.env.PULL_REQUEST === "true";
const prettierPath = isPullRequest ? "dist" : "node_modules/prettier/";

const parserPaths = parsers.map(stripLanguageDirectory);

// --- Build prettier for PR ---

if (isPullRequest) {
Expand All @@ -44,21 +39,19 @@ shell.exec(
`node_modules/babel-cli/bin/babel.js ${docs}/index.js --out-file ${docs}/index.js --presets=es2015`
);

shell.echo("Bundling docs babylon...");
shell.exec(
`rollup -c scripts/build/rollup.docs.config.js --environment filepath:parser-babylon.js -i ${prettierPath}/parser-babylon.js`
);
shell.exec(
`node_modules/babel-cli/bin/babel.js ${docs}/parser-babylon.js --out-file ${docs}/parser-babylon.js --presets=es2015`
);

for (const parser of parsers) {
if (parser === "babylon") {
for (const parser of parserPaths) {
if (parser.endsWith("babylon")) {
continue;
}
shell.echo(`Bundling docs ${parser}...`);
shell.exec(
`rollup -c scripts/build/rollup.docs.config.js --environment filepath:parser-${parser}.js -i ${prettierPath}/parser-${parser}.js`
`rollup -c scripts/build/rollup.docs.config.js --environment filepath:${parser}.js -i ${prettierPath}/${parser}.js`
);
}

Expand Down
21 changes: 4 additions & 17 deletions scripts/build/build.js
Expand Up @@ -5,19 +5,10 @@
const path = require("path");
const pkg = require("../../package.json");
const formatMarkdown = require("../../website/static/markdown");
const parsers = require("./parsers");
const shell = require("shelljs");

const rootDir = path.join(__dirname, "..", "..");
const parsers = [
"babylon",
"flow",
"typescript",
"graphql",
"postcss",
"parse5",
"markdown",
"vue"
];

process.env.PATH += path.delimiter + path.join(rootDir, "node_modules", ".bin");

Expand All @@ -32,30 +23,26 @@ shell.rm("-Rf", "dist/");

// --- Lib ---

shell.echo("Bundling lib index...");
shell.exec("rollup -c scripts/build/rollup.index.config.js");

shell.echo("Bundling lib bin...");
shell.exec("rollup -c scripts/build/rollup.bin.config.js");
shell.chmod("+x", "./dist/bin/prettier.js");

shell.echo("Bundling lib third-party...");
shell.exec("rollup -c scripts/build/rollup.third-party.config.js");

for (const parser of parsers) {
if (parser === "postcss") {
if (parser.endsWith("postcss")) {
continue;
}
shell.echo(`Bundling lib ${parser}...`);
shell.exec(
`rollup -c scripts/build/rollup.parser.config.js --environment parser:${parser}`
);
}

shell.echo("Bundling lib postcss...");
shell.echo("\nsrc/language-css/parser-postcss.js → dist/parser-postcss.js");
// PostCSS has dependency cycles and won't work correctly with rollup :(
shell.exec(
"webpack --hide-modules src/parser-postcss.js dist/parser-postcss.js"
"webpack --hide-modules src/language-css/parser-postcss.js dist/parser-postcss.js"
);
// Prepend module.exports =
const content = shell.cat("dist/parser-postcss.js").stdout;
Expand Down
12 changes: 12 additions & 0 deletions scripts/build/parsers.js
@@ -0,0 +1,12 @@
"use strict";

module.exports = [
"language-js/parser-babylon",
"language-js/parser-flow",
"language-js/parser-typescript",
"language-graphql/parser-graphql",
"language-css/parser-postcss",
"language-html/parser-parse5",
"language-markdown/parser-markdown",
"language-vue/parser-vue"
];
4 changes: 2 additions & 2 deletions scripts/build/rollup.bin.config.js
Expand Up @@ -24,9 +24,9 @@ export default Object.assign(baseConfig, {
"assert",
"util",
"events",
path.resolve("src/third-party.js")
path.resolve("src/common/third-party.js")
],
paths: {
[path.resolve("src/third-party.js")]: "../third-party"
[path.resolve("src/common/third-party.js")]: "../third-party"
}
});
4 changes: 2 additions & 2 deletions scripts/build/rollup.index.config.js
Expand Up @@ -8,7 +8,7 @@ import * as path from "path";
const external = ["assert"];

if (process.env.BUILD_TARGET !== "website") {
external.push(path.resolve("src/third-party.js"));
external.push(path.resolve("src/common/third-party.js"));
}

export default Object.assign(baseConfig, {
Expand All @@ -25,6 +25,6 @@ export default Object.assign(baseConfig, {
],
external,
paths: {
[path.resolve("src/third-party.js")]: "./third-party"
[path.resolve("src/common/third-party.js")]: "./third-party"
}
});
11 changes: 6 additions & 5 deletions scripts/build/rollup.parser.config.js
Expand Up @@ -4,15 +4,16 @@ import commonjs from "rollup-plugin-commonjs";
import json from "rollup-plugin-json";
import replace from "rollup-plugin-replace";
import uglify from "uglify-es";
import path from "path";

const parser = process.env.parser;

export default Object.assign(baseConfig, {
entry: "src/parser-" + parser + ".js",
dest: "dist/parser-" + parser + ".js",
entry: "src/" + parser + ".js",
dest: "dist/" + path.basename(parser) + ".js",
format: "cjs",
plugins: [
parser === "typescript"
parser.endsWith("typescript")
? replace({
"exports.Syntax =": "1,",
include: "node_modules/typescript-eslint-parser/parser.js"
Expand All @@ -21,7 +22,7 @@ export default Object.assign(baseConfig, {
// In flow-parser 0.59.0 there's a dynamic require: `require(s8)` which not
// supported by rollup-plugin-commonjs, so we have to replace the variable
// by its value before bundling.
parser === "flow"
parser.endsWith("flow")
? replace({
"require(s8)": 'require("fs")',
include: "node_modules/flow-parser/flow_parser.js"
Expand Down Expand Up @@ -50,5 +51,5 @@ export default Object.assign(baseConfig, {
"os",
"crypto"
],
useStrict: parser !== "flow"
useStrict: !parser.endsWith("flow")
});