π Pluggable and configurable JavaScript Linter, code transformer and formatter, drop-in ESLint superpower replacement πͺ with built-in support for js, jsx, typescript, flow, markdown, yaml and json. Write declarative codemods in a simplest possible way π
Perfection is finally attained not when there is no longer anything to add,
but when there is no longer anything to take away.Β© Antoine de Saint ExupΓ©ry
πPutout is a JavaScript Linter, pluggable and configurable code transformer, drop-in ESLint replacement with built-in code printer and ability to fix syntax errors. It has a lot of transformations that keeps your codebase in a clean state, removing any code smell and making code readable according to best practices.
The main target is JavaScript, but:
are also supported. Here is how it looks like:
CommonJS
to ESM
Check out couple variants of plugins that does the same: linting debugger statement:
'use strict';
module.exports.report = () => `Unexpected 'debugger' statement`;
module.exports.replace = () => ({
debugger: '',
});
Choose wisely, competitors cannot even fixβ¦ π€«
πPutout in addition to own format .putout.json
supports both eslint.config.js
and .eslintrc.json
, it has ability to autodect format you use.
Also it works good with monorepository, since it uses eslint.config.js
that is closer to linting file, instead of cwd
of ESLint run.
If I have seen further, it is by standing upon the shoulders of giants.
Β© Isaac Newton
πPutout on the other hand can make more drastic code transformations that directly affects your codebase making it a better place to code π»:
To install πPutout as a development dependency, run:
npm i putout -D
Make sure that you are running a relatively recent (β₯16) version of Node.
Grown-ups never understand anything by themselves, and it is tiresome for children to be always and forever explaining things to them.
Β© Antoine de Saint-ExupΓ©ry
πPutout tries to be clear and likes a lot to explain things. So when you write putout --help
most likely you will hear gladly purr :
Usage: putout [options] [path]
Options:
-h, --help display this help and exit
-v, --version output version information and exit
-f, --format [formatter] use a specific output format, the default is: 'progress-bar' locally and 'dump' on CI
-s, --staged add staged files when in git repository
-i, --interactive set lint options using interactive menu
--fix apply fixes of errors to code
--fix-count [count = 10] count of fixes rounds
--rulesdir use additional rules from directory
--transform [replacer] apply Replacer, for example 'var __a = __b -> const __a = __b', read about Replacer https://git.io/JqcMn
--plugins [plugins] a comma-separated list of plugins to use
--enable [rule] enable the rule and save it to '.putout.json' walking up parent directories
--disable [rule] disable the rule and save it to '.putout.json' walking up parent directories
--enable-all enable all found rules and save them to '.putout.json' walking up parent directories
--disable-all disable all found rules (set baseline) and save them to '.putout.json' walking up parent directories
--match [pattern] read '.putout.json' and convert 'rules' to 'match' according to 'pattern'
--flow enable flow
--fresh generate a fresh cache
--no-config avoid reading '.putout.json'
--no-ci disable the CI detection
--no-cache disable the cache
--no-worker disable worker thread
To skip prefix node_modules/.bin/
, update your $PATH
variable in with .bashrc
:
echo 'PATH="$PATH:node_modules/.bin"' >> ~/.bashrc
source ~/.bashrc
To find possible transform places in a folder named lib
, run:
putout lib
To find possible transform places in multiple folders, such as folders named lib
and test
, run:
putout lib test
To apply the transforms, use --fix
:
putout lib test --fix
Developers, myself included, usually prefer to make all code changes manually, so that nothing happens to our code without reviewing it first. That is until we trust a tool to make those changes safely for us. An example is WebStorm, which we trust when renaming a class
or a method
. Since πPutout may still feel like a new tool, not all of us will be able to trust it immediately.
A good way to gain trust is two run without --fix
option, and observe error messages. Another way is to use traditional version control tactics. Before running πPutout you should do a git commit
. Then after running πPutout, youβll be able to inspect the changes it made using git diff
and git status
. You still have the chance to run git checkout -- .
at any time to revert all the changes that πPutout has made. If you need more fine-grained control, you can also use git add -p
or git add -i
to interactively stage only the changes you want to keep.
πPutout supports the following environment variables:
PUTOUT_CONFIG_FILE
- path to configuration file;PUTOUT_FILES
- files that should be processed split by comma (,
);Example:
PUTOUT_FILES=lib,test putout --fix
When you need to run πPutout in Deno, use @putout/bundle
:
import putout from 'https://esm.sh/@putout/bundle';
import removeDebugger from 'https://esm.sh/@putout/plugin-remove-debugger?alias=putout:@putout/bundle';
import declare from 'https://esm.sh/@putout/plugin-declare?alias=putout:@putout/bundle';
putout('isFn(fn); debugger', {
plugins: [
['remove-debugger', removeDebugger],
['declare', declare],
],
});
// returns
({
code: `const isFn = a => typeof a === 'function';\nisFn(fn);`,
places: [],
});
When you need to change configuration file use Ruler instead of editing the file manually.
Ruler can:
putout --enable [rule]
;putout --disable [rule]
;putout --enable-all
;putout --disable-all
;βοΈRemember, Ruler should never be used with --fix
, because unclear things makes π Putout angry and you can find him barking at you:
π '--fix' cannot be used with ruler toggler ('--enable', '--disable')
You may want to convert your CommonJS
to ESM
since node v12 supports it without a flag.
CommonJS
to ESM
package.json
Well, if you have no type
field or type=commonjs
your package will be
converted to CommonJS
automatically. To convert to ESM
just set type=module
.
.cjs
or .mjs
filesThey will be converted automatically to CommonJS
and ESM
accordingly.
Letβs suppose you have a file called index.js
:
const unused = 5;
module.exports = function() {
return promise();
};
async function promise(a) {
return Promise.reject(Error('x'));
}
You call putout --fix index.js
and see that file is changed:
'use strict';
module.exports = async function() {
return await promise();
};
async function promise() {
throw Error('x');
}
But for some reason you donβt want so many changes.
βοΈ Remember, safe mode of eslint-plugin-putout has the most dangerous rules disabled, so it can be used as auto fix on each save in your IDE.
So, if you want to convert it to ESM
keeping everything else untouched use Ruler: it can easily disable all rules πPutout finds.
putout index.js --disable-all
will find next errors:
1:4 error 'unused' is defined but never used remove-unused-variables
7:23 error 'a' is defined but never used remove-unused-variables
3:0 error Use arrow function convert-to-arrow-function
1:0 error Add missing 'use strict' directive on top of CommonJS mode/add-missing
8:4 error Reject is useless in async functions, use throw instead promises/convert-reject-to-throw
4:11 error Async functions should be called using 'await' promises/add-missing-await
7:0 error Avoid useless async promises/remove-useless-async
It will create config file .putout.json
:
{
"rules": {
"remove-unused-variables": "off",
"convert-to-arrow-function": "off",
"nodejs/strict-mode-add-missing": "off",
"promises/convert-reject-to-throw": "off",
"promises/add-missing-await": "off",
"promises/remove-useless-async": "off"
}
}
Then running putout index.js --enable nodejs/convert-commonjs-to-esm
will update config with:
{
"rules": {
"remove-unused-variables": "off",
"convert-to-arrow-function": "off",
"nodejs/strict-mode-add-missing": "off",
"promises/convert-reject-to-throw": "off",
"promises/add-missing-await": "off",
- "promises/remove-useless-async": "off"
+ "promises/remove-useless-async": "off",
+ "nodejs/convert-commonjs-to-esm": "on"
}
}
Then putout --fix index.js
will do the thing and update index.js
with:
const unused = 5;
export default function() {
return promise();
}
async function promise(a) {
return Promise.reject(Error('x'));
}
So in case of src
directory, it will look like:
putout src --disable-all && putout src --enable nodejs/convert-commonjs-to-esm && putout src --fix
This command will disable all rules that πPutout can find right now and enable a single rule. All built-in rules made for good and highly suggested to be used, all of them are enabled in all my repositories, since they have auto fix.
βοΈYou can always disable what you donβt need, so give it a try. You wonβt regret π.
Happy coding π!
πPutout consists of a couple simple parts, here is a workflow representation:
And here is a CLI scheme:
The wise speak of the perennial Ashvattha tree,
which has roots above and branches below.
The leaves protecting it are the Vedas.
One who knows this, truly knows.
The tender sprouts of this mighty tree
are the senses nourished by the gunas.
The branches extend both above and below.
The secondary roots going downward represent actions
that bind the individual soul to earthly existence.Β© βBhagavatgitaβ, chapter 15
On the bottom level of πPutout layes down Syntax Tree. This is data structure that makes it possible to do crazy transformations in a simplest possible way. It is used mostly in compilers development.
You can read about it in Babel Plugin Handbook. To understand how things work from the inside take a look at Super Tiny Compiler.
Preoccupied with a single leaf, you wonβt see the tree.
Preoccupied with a single tree, youβll miss the entire forest.
When you look at a tree, see it for its leaves, its branches, its trunk and the roots, then and only then will you see the tree.Β© Takuan Soho, βThe Unfettered Mind: Writings of the Zen Master to the Sword Masterβ
Consider next piece of code:
hello = 'world';
It looks this way in ESTree JavaScript syntax format:
{
"type": "AssignmentExpression",
"operator": "=",
"left": {
"type": "Identifier",
"name": "hello"
},
"right": {
"type": "StringLiteral",
"value": "world"
}
}
When one is not capable of true intelligence, it is good to consult with someone of good sense. An advisor will fulfill the Way when he makes a decision by selfless and frank intelligence because he is not personally involved. This way of doing things will certainly be seen by others as being strongly rooted. It is, for example, like a large tree with many roots.
Β© Yamamoto Tsunetomo βHagakureβ
πPutout based on Babel AST. It has a couple of differences from ESTree which are perfectly handled by estree-to-babel
.
βοΈ You can get more information about AST in The Book of AST.
engines
chilling with engines
, and chasing plugins
, processors
, operators
;plugins
chilling with plugins
and operators
via require('putout').operator
;processors
chilling with processors
;operators
chilling with operators
;Engines is the heart of πPutout: Parser, Loader and Runner are running for every processed file. Processor runs all the processors.
Package | Version |
---|---|
@putout/engine-parser |
|
@putout/engine-loader |
|
@putout/engine-runner |
|
@putout/engine-processor |
|
@putout/engine-reporter |
With help of processors πPutout can be extended to read any file format and parse JavaScript from there.
Here is a list of built-int processors:
Package | Version |
---|---|
@putout/processor-javascript |
|
@putout/processor-json |
|
@putout/processor-markdown |
|
@putout/processor-ignore |
|
@putout/processor-yaml |
|
@putout/processor-css |
|
@putout/processor-filesystem |
You can disable any of them with:
{
"processors": [
["markdown", "off"]
]
}
Not bundled processors:
Package | Version |
---|---|
@putout/processor-typescript |
|
@putout/processor-html |
|
@putout/processor-wasm |
External processors:
Package | Version |
---|---|
putout-processor-typos |
To enable, install and use:
{
"processors": [
["typescript", "on"]
]
}
Processors can be tested using @putout/test/processors.
In oneβs life there are levels in the pursuit of study. In the lowest level, a person studies but nothing comes of it, and he feels that both he and others are unskillful. At this point he is worthless. In the middle level he is still useless but is aware of his own insufficiencies and can also see the insufficiencies of others. At a higher level, he has pride concerning his own ability, rejoices in praise from others, and laments the lack of ability in his fellows. This man has worth. At the highest level a man has the look of knowing nothing.
Β© Yamamoto Tsunetomo βHagakureβ
In the similar way works πPutout API: it has no
plugins defined, tabula rasa.
First things first, require
putout:
const putout = require('putout');
Letβs consider the next source
with two VariableDeclarations
and one CallExpression
:
const hello = 'world';
const hi = 'there';
console.log(hello);
We can declare it as source
:
const source = `
const hello = 'world';
const hi = 'there';
console.log(hello);
`;
πPutout supports dynamic loading of plugins from node_modules
. Letβs consider example of using the remove-unused-variables plugin:
putout(source, {
plugins: [
'remove-unused-variables',
],
});
// returns
({
code: `\n const hello = 'world';\n\n console.log(hello);\n`,
places: [],
});
As you see, places
is empty, but the code is changed: there is no hi
variable.
From the beginning, πPutout developed with ability to split the main process into two concepts: find
(find places that could be fixed) and fix
(apply the fixes to the files).
It is therefore easy to find sections that could be fixed.
In the following example redundant variables are found without making changes to the source file:
putout(source, {
fix: false,
plugins: [
'remove-unused-variables',
],
});
// returns
({
code: '\n' + ` const hello = 'world';\n` + ` const hi = 'there';\n` + ' \n' + ' console.log(hello);\n',
places: [{
rule: 'remove-unused-variables',
message: '"hi" is defined but never used',
position: {
line: 3,
column: 10,
},
}],
});
Source maps are embedded in the generated source using a special comment. These comments may contain the entire source map, using a Data URI, or may reference an external URL or file.
In our case Data URL
used. Here is an example of source map:
{
"version": 3,
"file": "out.js",
"sourceRoot": "",
"sources": [
"foo.js",
"bar.js"
],
"names": [
"src",
"maps",
"are",
"fun"
],
"mappings": "AAgBC,SAAQ,CAAEA"
}
To generate source map you need to pass:
sourceFileName
;sourceMapName
;putout(source, {
fix: false,
sourceFileName: 'hello.js',
sourceMapName: 'world.js',
plugins: [
'remove-unused-variables',
],
});
// returns
({
code: `
const hello = 'world';
const hi = 'there';
console.log(hello);
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJ...
`,
places: [{
rule: 'remove-unused-variables',
message: '"hi" is defined but never used',
position: {
line: 3,
column: 10,
},
}],
});
unused variables
function show() {
- const message = 'hello';
console.log('hello world');
}
for...of
variables-for (const {a, b} of c) {
+for (const {a} of c) {
console.log(a);
}
unreferenced variables
-let a;
- a = 1;
let b;
b = 2;
console.log(b);
keys
const a = {
- x: 'hello',
- ...y,
x: 'world',
...y,
}
case
switch (x) {
case 5:
console.log('hello');
break;
- case 5:
- console.log('zz');
- break;
}
private fields
class Hello {
#a = 5;
- #b = 3;
get() {
return this.#a;
};
}
expressions
function show(error) {
- showError;
}
variables
- function hi(a) {
- const b = a;
};
+ function hi(b) {
};
push
function notUsed() {
- const paths = [];
for (const [key, name] of tuples) {
- paths.push([key, full]);
}
}
Object.assign()
-const load = stub().rejects(assign(Error('LOAD USED')));
+const load = stub().rejects(Error('LOAD USED'));
replace()
-const a = 'hello'.replace(world, world);
+const a = 'hello';
new
(why)-new Error('something when wrong');
+Error('something when wrong');
new
-const map = Map();
+const map = new Map();
constructor
(why)class A extends B() {
- constructor(...args) {
- super(...args);
- }
}
map
-const [str] = lines.map((line) => `hello ${line}`);
+const [line] = lines;
+const str = `hello ${line}`;
continue
-for (sign = decpt, i = 0; (sign /= 10) != 0; i++)
- continue;
+for (sign = decpt, i = 0; (sign /= 10) != 0; i++);
operand
-a = a + b;
+a += b;
return
-module.exports.traverse = ({push}) => {
- return {
- ObjectExpression(path) {
- }
- }
-};
+module.exports.traverse = ({push}) => ({
+ ObjectExpression(path) {
+ }
+});
array
-A[[B]];
+A[B];
array constructor
-const a = Array(1, 2, 3);
+const a = [1, 2, 3];
conditions
-if (zone?.tooltipCallback) {
- zone.tooltipCallback(e);
-}
+zone?.tooltipCallback(e);
type conversion
-const a = Boolean(b.includes(c));
+const a = b.includes(c);
--if (!!a)
++if (a)
console.log('hi');
functions
-const f = (...a) => fn(...a);
-array.filter((a) => a);
+const f = fn;
+array.filter(Boolean);
typeof
- typeof typeof 'hello';
+ typeof 'hello';
reference
-const {compare} = operator;
import {operator} from 'putout';
+const {compare} = operator
imports
first-const [arg] = process.argv;
import esbuild from 'esbuild';
+const [arg] = process.argv;
variables
+const fs = import 'fs/promises';
+const {stub} = import 'supertape';
+const {assign} = Object;
const readFile = stub();
assign(fs, {
readFile,
});
arguments
onIfStatement({
push,
- generate,
- abc,
})
function onIfStatement({push}) {
}
template expressions
-let y = `${"hello"} + ${"world"}`;
+let y = `hello + world`;
for...of
-for (const a of ['hello']) {
- console.log(a);
-}
+console.log('hello');
array.entries()
-for (const [, element] of array.entries()) {
-}
+for (const element of array) {
+}
const putout = require('putout');
-const {operator} = require('putout');
+const {operator} = putout;
assignment
to arrow function
-const createRegExp = (a) = RegExp(a, 'g');
+const createRegExp = (a) => RegExp(a, 'g');
assignment
to comparison
-if (a = 5) {
+if (a === 5) {
}
arrow function
to condition
-if (a => b) {}
+if (a >= b) {}
quotes
to backticks
-const a = 'hello \'world\'';
+const a = `hello 'world'`;
typeof
to is type
+const isFn = (a) => typeof a === 'function';
+
+if (isFn(fn))
-if (typeof fn === 'function')
fn();
bitwise
to logical
-a | !b
+a || !b
equal
to strict equal
-if (a == b) {
+if (a === b) {
}
escape
-const t = 'hello \"world\"';
-const s1 = `hello \"world\"`;
-const s = `hello \'world\'`;
+const t = 'hello "world"';
+const s1 = `hello "world"`;
+const s = `hello 'world'`;
Array.from()
-for (const x of Array.from(y)) {}
+for (const x of y) {}
spread
-for (const x of [...y]) {}
+for (const x of y) {}
debugger
statement- debugger;
iife
-(function() {
- console.log('hello world');
-}());
+console.log('hello world');
boolean
from assertions
-if (a === true)
+if (a)
alert();
boolean
from logical expressions
-const t = true && false;
+const t = false;
for (const x of Object.keys(a)) {
- {
- console.log(x);
- }
+ console.log(x);
}
function hi() {
return 5;
- console.log('hello');
}
-let a, b;
+let a;
+let b;
destructuring
-const {a: {b}} = c;
+const {a} = c;
+const {b} = a;
assignment
-const {a} = {a: 5};
-const [b] = [5];
+const a = 5;
+const b = 5;
boolean return
function isA(a, b) {
- if (a.length === b.length)
- return true;
-
- return false;
+ return a.length === b.length;
}
logical expressions
-!(options && !options.bidirectional);
+!options || options.bidirectional;
ternary
-module.exports = fs.copyFileSync ? fs.copyFileSync : copyFileSync;
+module.exports = fs.copyFileSync || copyFileSync;
console.log
calls-console.log('hello');
-if (x > 0) {
-}
-const {} = process;
constant conditions
function hi(a) {
- if (2 < 3) {
- console.log('hello');
- console.log('world');
- }
+ console.log('hello');
+ console.log('world');
};
function world(a) {
- if (false) {
- console.log('hello');
- console.log('world');
- }
};
replace
to replaceAll
(stage-4)-'hello'.replace(/hello/g, 'world');
+'hello'.replaceAll('hello', 'world');
-if (a)
+if (a) {
b();
+} else {
-else {
c();
d();
}
-const hello = world.hello;
-const a = b[0];
+const {hello} = world;
+const [a] = b;
-a['hello']['world'] = 5;
+a.hello.world = 5;
.startsWith()
const {a = ''} = b;
-!a.indexOf('>');
+a.startsWith('>');
overrides
-export const readRules = (dirOpt, rulesDir, {cwd, readdirSync}) => {}
+export const readRules = (dirOpt, rulesDir, overrides) => {
const {cwd, readdirSync} = overrides;
+}
+import a1 from 'a1';
import {
a,
b,
c,
d,
} from 'd';
-import a1 from 'a1';
template literals
-const line = 'hello' + world;
+const line = `hello${world}`
flatMap()
-array.map(getId).flat();
+array.flatMap(getId);
if condition
-if (2 > 3);
+if (2 > 3)
alert();
isArray()
-x instanceof Array;
+Array.isArray(x);
Array.at()
-const latest = (a) => a[a.length - 1];
+const latest = (a) => a.at(-1);
-const result = hello && hello.world;
+const result = hello?.world;
-result = typeof result === 'undefined' ? 'hello': result;
result = result ?? 'hello';
throw
statement into expression (proposal-throw-expressions, not bundled)-const fn = (a) => {throw Error(a);}
+const fn = (a) => throw Error(a);
-const {one} = require('numbers'):
-const {two} = require('numbers');
+ const {
+ one,
+ two
+} = require('numbers');
-import {m as b} from 'y';
-import {z} from 'y';
-import x from 'y';
+import x, {m as b, z} from 'y';
const isFn = (a) => typeof a === 'function';
-const isFn1 = (a) => typeof a === 'function';
isFn(1);
-isFn1(2);
+isFn(2);
if
statements-if (a > b)
- if (b < c)
- console.log('hi');
+if (a > b && b < c)
+ console.log('hi');
anonymous
to arrow function
-module.exports = function(a, b) {
+module.exports = (a, b) => {
}
for
to for...of
-for (let i = 0; i < items.length; i++) {
+for (const item of items) {
- const item = items[i];
log(item);
}
forEach
to for...of
-Object.keys(json).forEach((name) => {
+for (const name of Object.keys(json)) {
manage(name, json[name]);
-});
+}
for...in
to for...of
-for (const name in object) {
- if (object.hasOwnProperty(name)) {
+for (const name of Object.keys(object)) {
console.log(a);
- }
}
map
to for...of
-names.map((name) => {
+for (const name of names) {
alert(`hello ${name}`);
+}
-});
reduce
to for...of
-const result = list.reduce((a, b) => a + b, 1);
+let sum = 1;
+for (const a of list) {
+ sum += a;
+}
array copy
to slice
-const places = [
- ...items,
-];
+const places = items.slice();
-module.exports.x = 1,
-module.exports.y = 2;
+module.exports.x = 1;
+module.exports.y = 2;
-const {replace} = putout.operator;
-const {isIdentifier} = putout.types;
+const {operator, types} = putout;
+const {replace} = operator;
+const {isIdentifier} = types;
apply
to spread
-console.log.apply(console, arguments);
+console.log(...arguments);
concat
to flat
-[].concat(...array);
+array.flat();
arguments
to rest
-function hello() {
- console.log(arguments);
+function hello(...args) {
+ console.log(args);
}
Object.assign()
to merge spread
function merge(a) {
- return Object.assign({}, a, {
- hello: 'world'
- });
+ return {
+ ...a,
+ hello: 'world'
+ };
};
comparison
to boolean
- const a = b === b;
+ const a = true;
-5 === a;
+a === 5;
const
to let
- const a = 5;
+ let a = 5;
a = 3;
label
to object
-const a = () => {
- hello: 'world'
-}
+const a = () => ({
+ hello: 'world'
+})
labels
-hello:
while (true) {
break;
}
await
- await await Promise.resolve('hello');
+ await Promise.resolve('hello');
async
-const show = async () => {
+const show = () => {
console.log('hello');
};
await
-runCli();
+await runCli();
async function runCli() {
}
async
-function hello() {
+async function hello() {
await world();
}
await
to return promise()
statements (because it's faster, produces call stack and more readable)async run () {
- return promise();
+ return await promise();
}
import fs from 'fs';
-(async () => {
- const data = await fs.promises.readFile('hello.txt');
-})();
+const data = await fs.promises.readFile('hello.txt');
Promise.resolve()
async () => {
- return Promise.resolve('x');
+ return 'x';
}
Promise.reject()
to throw
async () => {
- return Promise.reject('x');
+ throw 'x';
}
await import()
-const {readFile} = import('fs/promises');
+const {readFile} = await import('fs/promises');
-const a = 100000000;
+const a = 100_000_000;
Math.sqrt()
to Math.hypot()
-const a = Math.sqrt(b ** 2 + c ** 2);
+const a = Math.hypot(a, b);
Math.imul()
to multiplication
- const a = Math.imul(b, c);
+ const a = b * c;
Math.pow
to exponentiation operator
-Math.pow(2, 4);
+2 ** 4;
strict mode
directive from esm-'use strict';
-
import * from fs;
strict mode
directive in commonjs
if absent+'use strict';
+
const fs = require('fs');
strict mode
directive in commonjs
if absent+'use strict';
+
const fs = require('fs');
strict mode
directive from esm-'use strict';
-
import * from fs;
strict mode
directive in commonjs
if absent+'use strict';
+
const fs = require('fs');
strict mode
directive from esm-'use strict';
-
import * from fs;
strict mode
directive in commonjs
if absent+'use strict';
+
const fs = require('fs');
strict mode
directive from esm-'use strict';
-
import * from fs;
strict mode
directive in commonjs
if absent+'use strict';
+
const fs = require('fs');
esm
to commonjs
(disabled)-import hello from 'world';
+const hello = require('world');
commonjs
to esm
(disabled)-const hello = require('world');
+import hello from 'world';
fs.promises
to fs/promises
for node.js-const {readFile} = require('fs').promises;
+const {readFile} = require('fs/promises');
top-level return
into process.exit()
(because EcmaScript Modules doesn't support top level return)- return;
+ process.exit();
process.exit
call-process.exit();
test.only
with test
calls-test.only('some test here', (t) => {
+test('some test here', (t) => {
t.end();
});
test.skip
with test
calls-test.skip('some test here', (t) => {
+test('some test here', (t) => {
t.end();
});
union
-type x = boolean[] | A | string | A | string[] | boolean[];
+type x = boolean[] | A | string | string[];
generic
to shorthand
(why)interface A {
- x: Array<X>;
+ x: X[];
}
types
from constants
-const x: any = 5;
+const x = 5;
mapped types
-type SuperType = {
- [Key in keyof Type]: Type[Key]
-}
+type SuperType = Type;
mapping modifiers
type SuperType = {
- +readonly[Key in keyof Type]+?: Type[Key];
+ readonly[Key in keyof Type]?: Type[Key];
}
types
type oldType = number;
-type newType = oldType;
-const x: newType = 5;
+const x: oldType = 5;
interface
keysinterface Hello {
- 'hello': any;
'hello': string;
}
types
type n = number;
-type s = string;
const x: n = 5;
as
type assertion (according to best practices)-const boundaryElement = <HTMLElement>e.target;
+const boundaryElement1 = e.target as HTMLElement;
-type SuperType = {
- [Key in keyof Type]?: Type[Key];
-}
+type SuperType = Partial<Type>;
The πPutout repo is comprised of many npm packages. It is a Lerna monorepo similar to Babel.
It has a lot of plugins divided by groups:
Package | Version |
---|---|
@putout/plugin-sort-imports-by-specifiers |
Package | Version |
---|---|
@putout/plugin-split-assignment-expressions |
|
@putout/plugin-split-variable-declarations |
|
@putout/plugin-split-nested-destructuring |
Package | Version |
---|---|
@putout/plugin-merge-destructuring-properties |
|
@putout/plugin-merge-duplicate-imports |
|
@putout/plugin-merge-duplicate-functions |
Package | Version |
---|---|
@putout/plugin-simplify-assignment |
|
@putout/plugin-simplify-ternary |
|
@putout/plugin-simplify-boolean-return |
Package | Version |
---|---|
@putout/plugin-declare |
|
@putout/plugin-declare-imports-first |
|
@putout/plugin-declare-before-reference |
Package | Version |
---|---|
@putout/plugin-extract-sequence-expressions |
|
@putout/plugin-extract-object-properties |
Package | Version |
---|---|
@putout/plugin-reuse-duplicate-init |
Package | Version |
---|---|
@putout/plugin-group-imports-by-source |
Next packages not bundled with πPutout but can be installed separately.
πPutout uses formatters similar to ESLintβs formatters.
You can specify a formatter using the --format
or -f
flag on the command line. For example, --format codeframe
uses the codeframe
formatter.
The built-in formatter options are:
dump
stream
json
json-lines
codeframe
progress
progress-bar
frame
(codeframe
+ progress
)memory
time
A formatter function executes on every processed file, it should return an output string
.
export default function formatter({name, source, places, index, count, filesCount, errorsCount}) {
return '';
}
Here is list of options:
name
- name of processed filesource
- source code of processed fileindex
- current indexcount
- processing files countfilesCount
- count of files with errorserrorsCount
count of errorsYou can avoid any of this and use only what you need. To make your formatter usable with putout
, add the prefix putout-formatter-
to your npm
package,
and add the tags putout
, formatter
, putout-formatter
.
ESLint formatters can be used as well with help of @putout/formatter-eslint
this way:
Install:
npm i putout @putout/formatter-eslint eslint-formatter-pretty -D
Run:
ESLINT_FORMATTER=pretty putout -f eslint lib
To configure πPutout add a section named putout
to your package.json
file or create .putout.json
file and override any of default options.
All rules located in plugins
section and built-in rules are enabled by default.
You can disable rules using "off"
, or enable them (in match
section) using "on"
.
{
"rules": {
"remove-unused-variables": "off"
}
}
Or pass options using rules
section:
{
"rules": {
"remove-unused-variables": ["on", {
"exclude": "const global = __"
}]
}
}
With help of exclude
you can set type
or code pattern
to exclude for current rule.
Pass an array when you have a couple templates to exclude:
{
"rules": {
"remove-unused-variables": ["on", {
"exclude": [
"VariableDeclaration"
]
}]
}
}
exclude
is cross-plugin function supported by core, when develop your plugin, please use other name
to keep users ability to customize all plugins in a way they need to.
When you need to match paths to rules you can use match
section for this purpose in .putout.json
:
{
"match": {
"server": {
"nodejs/remove-process-exit": "on"
}
}
}
When you need to ignore some routes no matter what, you can use ignore
section in .putout.json
:
{
"ignore": [
"test/fixture"
]
}
In the eyes of mercy, no one should have hateful thoughts. Feel pity for the man who is even more at fault. The area and size of mercy is limitless.
Β© Yamamoto Tsunetomo βHagakureβ
You have also ability to define printer
of your choose, it can be:
@putout/printer
used by default, if you want to set any other update .putout.json
with:
{
"printer": "recast"
}
@putout/printer
:
recast
;recast
;recast
:
eslint-plugin-putout
;babel
:
recast
;You can choose any of them, but preferred is default printer.
There are two types of plugin names supported by πPutout, their names in npm start with a prefix:
@putout/plugin-
for official pluginsputout-plugin-
for user pluginsExample
If you need to remove-something
create putout
plugin with a name putout-plugin-remove-something
and add it to .putout.json
:
{
"plugins": [
"remove-something"
]
}
Add putout
as a peerDependency
to your packages.json
(>= of version you developing for).
βοΈ Always add keywords putout
, putout-plugin
when publish putout plugin to npm
so others can easily find it.
Throughout your life advance daily, becoming more skillful than yesterday more skillful than today. This is never-ending
Β© Yamamoto Tsunetomo βHagakureβ
πPutout plugins are the simplest possible way to transform AST
and this is for a reason.
And the reason is JavaScript-compatible language π¦PutoutScript which adds additional meaning to identifiers used in AST
-template.
Letβs dive into plugin types that you can use for you next code transformation.
The simplest πPutout plugin type consists of 2 functions:
report
- report error message to putout
cli;replace
- replace key
template into value
template;module.exports.report = () => 'use optional chaining';
module.exports.replace = () => ({
'__a && __a.__b': '__a?.__b',
});
This plugin will find and suggest to replace all occurrences of code: object && object.property
into object?.property
.
More powerful plugin type, when you need more control over traversing.
It should contain next 2 functions:
report
- report error message to putout
cli;fix
- fixes paths using places
array received using find
function;and one or more of this:
filter
- filter path, should return true
, or false
(donβt use with traverse
);include
- returns array of templates, or node names to include;exclude
- returns array of templates, or node names to exclude;module.exports.report = () => 'use optional chaining';
module.exports.include = () => ['debugger'];
module.exports.fix = (path) => {
path.remove(path);
};
βοΈ Use yeoman generator yo putout
, it will generate most of the plugin for you.
βοΈ More information about supported plugin types you can find in @putout/engine-runner.
βοΈ Find out about the way plugins load in @putout/engine-loader.
βοΈ When you need, you can use @babel/types, template and generate. All of this can be gotten from πPutout:
const {
types,
template,
generate,
} = require('putout');
When you need to use replaceWith
, replaceWithMultiple
, or insertAfter
, please use operator
instead of path
-methods.
const {template, operator} = require('putout');
const {replaceWith} = operator;
const ast = template.ast(`
const str = 'hello';
`);
module.exports.fix = (path) => {
// wrong
path.replaceWith(ast);
// correct
replaceWith(path, ast);
};
This should be done to preserve loc
and comments
information, which is different in Babel and Recast. πPutout will handle this case for you π,
just use the methods of operator
.
When you work on a plugin
or codemod
please add rule putout
into .putout.json
:
{
"rules": {
"putout": "on"
}
}
@putout/plugin-putout will handle plugin-specific cases for you π.
Letβs consider simplest possible plugin for removing debugger statements
@putout/plugin-remove-debugger:
// this is a message to show in putout cli
module.exports.report = () => 'Unexpected "debugger" statement';
// let's find all "debugger" statements and replace them with ""
module.exports.replace = () => ({
debugger: '',
});
Visitor
used in traverse function
can be code template as well. So when you need to find module.exports = <something>
, you
can use:
module.exports.traverse = ({push}) => ({
'module.exports = __'(path) {
push(path);
},
});
Where __
is a placeholder for anything.
βοΈRemember: template key should be valid JavaScript, or Node Type, like in previous example.
You can also use include
and/or exclude
instead of traverse
and filter
(more sophisticated example):
// should be always used include/or exclude, when traverse not used
module.exports.include = () => ['debugger'];
// optional
module.exports.exclude = () => [
'console.log',
];
// optional
module.exports.filter = (path) => {
// do some checks
return true;
};
There is predefined placeholders:
__
- any code;"__"
- any string literal;__
- any template string literal;That was the simplest module to remove debugger
statements in your code. Letβs look how to test it using @putout/test:
const removeDebugger = require('..');
const test = require('@putout/test')(__dirname, {
'remove-debugger': removeDebugger,
});
// this is how we test that messages is correct
test('remove debugger: report', (t) => {
t.reportCode('debugger', 'Unexpected "debugger" statement');
t.end();
});
// statement should be removed so result is empty
test('remove debugger: transformCode', (t) => {
t.transformCode('debugger', '');
t.end();
});
As you see test runner it is little bit extended πΌSupertape.
To see a more sophisticated example look at @putout/plugin-remove-console.
If you donβt want to publish a plugin you developed, you can pass it to πPutout as an object
described earlier. Here is how it can look like:
putout('const a = 5', {
plugins: [
['remove-unused-variables', require('@putout/plugin-remove-unused-variables')],
],
});
Where plugins
is an array
that contains [name, implementation]
tuples
.
πPutout supports codemodes
in the similar to plugins way, just create a directory ~/.putout
and put your plugins there. Here is example: convert-tape-to-supertape and this is example of work.
rulesdir
When you have plugins related to your project and you donβt want to publish them (because it cannot be reused right now). Use rulesdir
:
putout --rulesdir ./rules
This way you can keep rules specific for your project and run them on each lint.
βοΈ Remember: if you want to exclude file from loading, add prefix not-rule-
and πPutout will ignore it (in the same way as he does for node_modules
).
Find and fix problems in your JavaScript code
Β© eslint.org
If you see that πPutout breaks formatting of your code, use ESLint plugin eslint-plugin-putout.
Install eslint-plugin-putout
with:
npm i eslint eslint-plugin-putout -D
Then create .eslintrc.json
:
{
"extends": [
"plugin:putout/recommended"
],
"plugins": ["putout"]
}
And use with πPutout this way:
putout --fix lib
To set custom config file for ESLint use ESLINT_CONFIG_FILE
env variable:
ESLINT_CONFIG_FILE=test.eslintrc.json putout --fix lib
To disable ESLint support use NO_ESLINT
env variable:
NO_ESLINT=1 putout --fix lib
If you want to ignore ESLint warnings (if you for some reason have annoying unfixable errors π€·) use NO_ESLINT_WARNINGS=1
:
NO_ESLINT_WARNINGS=1 putout --fix lib
You can even lint without CLI using ESlint only, since πPutout is bundled to eslint-plugin-putout
:
eslint --fix lib
Applies πPutout transformations for you π.
ESLint begins his work as a formatter when πPutout done his transformations. Thatβs why it is used a lot in different parts of application, for testing purpose and using API in a simplest possible way. You can access it using @putout/eslint
:
import eslint from '@putout/eslint';
To use it simply write:
const [source, places] = await eslint({
name: 'hello.js',
code: `const t = 'hi'\n`,
fix: false,
});
Doesnβt it look similar to πPutout way? It definitely is! Butβ¦ It has a couple of differences you should remember:
code
and places
properties.name
property that is used to calculate configuration file.And you can even override any of ESLint βοΈ options with help of config
property:
const [source, places] = await eslint({
name: 'hello.js',
code: `const t = 'hi'\n`,
fix: false,
config: {
extends: [
'plugin:putout/recommended',
],
},
});
If you want to apply πPutout transformations using putout/putout
ESLint rule, enable πPutout with the same called flag lowercased:
const [source, places] = await eslint({
name: 'hello.js',
code: `const t = 'hi'\n`,
fix: true,
putout: true,
config: {
extends: [
'plugin:putout/recommended',
],
},
});
It is disabled by default, because ESLint always runs after πPutout transformations, so there is no need to traverse tree again.
π Putout can be used as babel plugin.
Just create .babelrc.json
file with configuration you need.
{
"plugins": [
["putout", {
"rules": {
"remove-unused-variables": "off"
}
}]
]
}
Since πPutout has dynamic nature of loading:
plugins
;processors
;formatters
;It was a nice adventure to add support of such a wonderful feature of Yarn
as Plug'n'Play
.
For this purpose new env variable
was added to help to load external extensions: PUTOUT_YARN_PNP
.
So if you use package eslint-config-hardcore you should run ESLint this way:
PUTOUT_YARN_PNP=eslint-config-hardcore eslint .
πPutout can be used as loader this way:
node --import putout/register your-file.js
You can also transform input files using Babel
. For example if you need to transform jsx
with @babel/plugin-transform-react-jsx
you can use .putout.json
:
{
"plugins": [
"babel/transform-react-jsx"
]
}
πPutout can have one of next exit codes:
Code | Name | Description | Output Example |
---|---|---|---|
0 | OK |
no errors found | <empty> |
1 | PLACE |
found places with errors | <violations of rules> |
2 | STAGE |
nothing in stage | <empty> |
3 | NO_FILES |
no files found | π No files matching the pattern "hello" were found |
4 | NO_PROCESSORS |
no processor found | π No processors found for hello.abc |
5 | NO_FORMATTER |
no formatter found | π Cannot find module 'putout-formatter-hello' |
6 | WAS_STOP |
was stop | <empty or violations of rules> |
7 | INVALID_OPTION |
invalid option | π Invalid option '--hello'. Perhaps you meant '--help' |
8 | CANNOT_LOAD_PROCESSOR |
processor has errors | <unhandled exception> |
9 | CANNOT_LOAD_FORMATTER |
formatter has errors | π @putout/formatter-dump: Syntax error |
10 | RULLER_WITH_FIX |
ruller used with --fix |
π '--fix' cannot be used with ruler toggler ('--enable', '--disable') |
11 | RULLER_NO_FILES |
ruller used without files | π 'path' is missing for ruler toggler ('--enable-all', '--disable-all') |
12 | INVALID_CONFIG |
config has invalid properties | π .putout.json: exclude: must NOT have additional properties |
13 | UNHANDLED |
unhandled exception | <unhandled exception> |
14 | CANNOT_LINT_STAGED |
cannot lint staged | π --staged: not git repository |
15 | INTERACTIVE_CANCELED |
interactive canceled | <empty> |
Example of providing invalid option:
coderaiser@localcmd:~/putout$ putout --hello
π Invalid option `--hello`. Perhaps you meant `--help`
coderaiser@localcmd:~/putout$ echo $?
7
Exit codes enum
can be imported as:
import {OK} from 'putout/exit-codes';
Are you also use πPutout in your application? Please open a Pull Request to include it here. We would love to have it in our list.
Putout follows semantic versioning (semver) principles, with version numbers being on the format major.minor.patch:
bug fix
, dependency update
(17.0.0 -> 17.0.1
).new features
, new plugins
or fixes
(17.0.0 -> 17.1.0
).breaking changes
, plugins remove
(17.0.0 -> 18.0.0
).You can contribute by proposing a feature, fixing a bug or a typo in the documentation.
If you wish to play with code π₯, you can πͺ!
π Putout rejoice and wag its tail when see new contributions πΎ.
MIT