नित्य आत्माव्यय: शुद्ध: सर्वग: सर्ववित्पर: ।
धत्तेऽसावात्मनो लिङ्गं मायया विसृजन्गुणान् ॥ २२ ॥nitya ātmāvyayaḥ śuddhaḥ
sarvagaḥ sarva-vit paraḥ
dhatte ’sāv ātmano liṅgaṁ
māyayā visṛjan guṇānThe spirit soul, the living entity, has no death, for he is eternal and inexhaustible. Being free from material contamination, he can go anywhere in the material or spiritual worlds. He is fully aware and completely different from the material body, but because of being misled by misuse of his slight independence, he is obliged to accept subtle and gross bodies created by the material energy and thus be subjected to so-called material happiness and distress. Therefore, no one should lament for the passing of the spirit soul from the body.
Hi folks 🎈!
The time is come for a new major release of 🐊Putout, pluggable and configurable JavaScript Linter, code transformer and formatter. This release has a couple breaking changes and some new features, so get a cup of hot tea ☕️ and enjoy reading!
📛 transform, transformAsync, findPlaces and findPlacesAsync no longer requires source argument
When you run putout('const a = 3') behind the scene there is a couple operations: parse, transform and print.
For a long time signature of transform looked this way:
transform(ast, source, options);
Why do we need source just after parse? - you ask. To keep tracking of shebang:
In computing shebang is the character sequence
#!, consisting of the characters number sign (also known as sharp or hash) and exclamation mark (also known as bang), at the beginning of a script.(c) wiki
So all flow now looks like this:
const source = parse(ast);
transform(ast, options);
const code = print(ast);
For a long time parsers like acorn, Babel and Espree (of ESLint) could not parse it, but with Hashbang Proposal everything changed, also recast couldn't handle hashbang but it is not longer supported so
there is not more need of such a strange way to keep line numbers correct 🎉.
As usual all changes handled by putout/remove-useless-source-argument.
📛 @putout/filesystem a bit simplified
@putout/plugin-filesystem a bit simplified:
{
"rules": {
- "filesystem/remove-travis-yml-file": "off",
- "filesystem/remove-vim-swap-file": "off",
- "filesystem/remove-ds-store-file": "off",
- "filesystem/remove-empty-directory": "off",
- "filesystem/remove-nyc-output": "off",
+ "filesystem/remove-files": "off",
+ "coverage/remove-files": "off",
}
}
If you changed .putout.json all modifications is handled by @putout/plugin-putout-config 😏.
📛 Changes in default ignore
Default ignores changed:
{
"ignore": [
"**/*.lock",
"**/*.log",
"**/.nyc_output/*",
"**/.yarn",
"**/.pnp.*",
"**/.idea",
"**/.git",
"**/package-lock.json",
"**/node_modules",
"**/fixture",
- "**/coverage",
+ "**/coverage/*",
"**/dist",
"**/dist-dev",
"**/build"
],
}
Now 🐊Putout sees coverage as directory, but ignores any files inside:
total 32136
drwxr-xr-x 13 coderaiser staff 416B Feb 17 16:23 ./
drwxr-xr-x 3 coderaiser staff 96B Feb 17 16:23 ../
-rw------- 1 coderaiser staff 6.7M Feb 17 16:23 coverage-95233-1771338202619-0.json
-rw------- 1 coderaiser staff 88K Feb 17 16:23 coverage-95234-1771338198859-0.json
-rw------- 1 coderaiser staff 1.3M Feb 17 16:23 coverage-95235-1771338199593-1.json
-rw------- 1 coderaiser staff 460K Feb 17 16:23 coverage-95235-1771338199605-0.json
-rw------- 1 coderaiser staff 1.3M Feb 17 16:23 coverage-95236-1771338199923-1.json
-rw------- 1 coderaiser staff 460K Feb 17 16:23 coverage-95236-1771338199936-0.json
-rw------- 1 coderaiser staff 1.3M Feb 17 16:23 coverage-95237-1771338200265-1.json
-rw------- 1 coderaiser staff 384K Feb 17 16:23 coverage-95237-1771338200276-0.json
-rw------- 1 coderaiser staff 1.2M Feb 17 16:23 coverage-95239-1771338200538-0.json
-rw------- 1 coderaiser staff 1.2M Feb 17 16:23 coverage-95240-1771338200803-0.json
-rw------- 1 coderaiser staff 1.2M Feb 17 16:23 coverage-95241-1771338201056-0.json
We don't want to lint them, but RedLint needs to see coverage directory so he can remove it with @putout/coverage/remove-files
we will came back to this is in a couple minutes.
📛 Migrated @putout/operator-ignore to Traverser
A
.gitignorefile specifies intentionally untracked files that Git should ignore.(c) git-scm.com
@putout/operator-ignore simplifies creating ignore-plugins.
It was Replacer from the beginning, it is one of the easiest plugins to write, it is always had report and replace, but can also contain match to filter nodes that we want to ignore. Here is how it looked like:
export const ignore = (type, {name, property, list}) => {
const [, collector] = type.split(/[()]/);
return {
report: createReport(name),
match: createMatch({
type,
property,
collector,
list,
}),
replace: createReplace({
type,
property,
collector,
list,
}),
};
};
const createMatch = ({type, property, collector, list}) => ({options}) => {
const {dismiss = []} = options;
const newNames = filterNames(list, dismiss);
return {
[type]: (vars) => {
const elements = parseElements(vars, {
property,
collector,
});
if (!elements)
return false;
const list = elements.map(getValue);
for (const name of newNames) {
if (!list.includes(name))
return true;
}
return false;
},
};
};
const createReplace = ({type, property, collector, list}) => ({options}) => {
const {dismiss = []} = options;
const newNames = filterNames(list, dismiss);
return {
[type]: (vars, path) => {
const elements = parseElements(vars, {
property,
collector,
});
const list = elements.map(getValue);
for (const name of newNames) {
if (!list.includes(name))
elements.push(stringLiteral(name));
}
return path;
},
};
};
As you can see part of code is duplicated. It runs twice:
- to filter things out;
- to replace things up;
In such cases much better to use Traverser.
Now what we have is:
const difference = (a, b) => new Set(a).difference(new Set(b));
export const ignore = ({name, property, list, type = __ignore}) => ({
report: createReport(name),
fix,
traverse: createTraverse({
type,
property,
list,
}),
});
const addQuotes = (a) => `'${a}'`;
const createReport = (filename) => ({name, matchedElements}) => {
let insteadOf = '';
if (matchedElements.length) {
const replacedNames = matchedElements.map(getValue);
const namesLine = replacedNames
.map(addQuotes)
.join(', ');
insteadOf = ` instead of ${namesLine}`;
}
return `Add '${name}'${insteadOf} to '${filename}'`;
};
export const fix = ({path, name, matchedElements}) => {
path.node.elements.push(stringLiteral(name));
matchedElements.map(remove);
};
const createTraverse = ({type, property, list}) => ({push, options}) => {
const {dismiss = []} = options;
const newNames = filterNames(list, dismiss);
if (!newNames.length)
return {};
return {
[type]: (path) => {
const [parentOfElements, elements] = parseElements(path, {
property,
});
if (!parentOfElements)
return;
const list = elements.map(getValue);
for (const name of difference(newNames, list)) {
const match = picomatch(name);
const matchedElements = [];
for (const current of elements.filter(exists)) {
const {value} = current.node;
if (match(value))
matchedElements.push(current);
}
push({
path: parentOfElements,
matchedElements,
elements,
name,
});
}
},
};
};
It has a couple benefits:
- ✅ We have more control over
report; - ✅ We have no duplication, all checks works only once in
traverseand if it is OK -fix;
One of the new features for @putout/operator-ignore is when you add mask *.log and your ignore file already has yarn-error.log, this both values are merged (that's right! not inserted and then with other rule cleaned up, but merged).
Since the best way to modify ignore files is using group rules - it is made in operator. If you have any kind of concerns about it - I'll be happy to discuss 😏.
☝️ Take away: you should always use the simplest possible way to write transformation, and later, when you have tests written, and see places to improve - refactor and make code better 😏.
☝️ As always, this case is handled for you in @putout/plugin-putout and will be auto fixed on next lint 😏
📛 Dropped support of ESLint < v10
eslint-plugin-putout requires ESLint >= v10 it is just released, so is better to upgrade.
Babel also already supports it, so the time is come 🤷♂️.
🔥crawlFile - new way of searching files
When you need to find files related to other files like in esm/apply-name-to-imported-file you can end up with:
export const report = () => 'Add file';
export const fix = (file) => {
writeFileContent(file, 'hello');
};
export const scan = (rootPath, {push, trackFile}) => {
for (const file of trackFile(rootPath, 'hello.txt')) {
findFile(rootPath, 'again and again');
}
};
It will be very slow! And you can use crawlDirectory() instead:
export const report = () => 'Add file';
export const fix = (file) => {
writeFileContent(file, 'hello');
};
export const scan = (rootPath, {push, trackFile}) => {
const crawled = crawlDirectory(rootPath);
for (const file of trackFile(rootPath, 'hello.txt')) {
findFile(rootPath, 'again and again', {
crawled,
});
}
};
But it is easy to forget, or remove during refactoring, since everything will work as before, only twice slower on big amount of files.
So you better use crawlFile:
export const report = () => 'Add file';
export const fix = (file) => {
writeFileContent(file, 'hello');
};
export const scan = (rootPath, {push, trackFile, crawlFile}) => {
for (const file of trackFile(rootPath, 'hello.txt')) {
const files = crawlFile(rootPath, 'no matter how many times');
}
};
It works much faster, use it when you do not mutate filesystem tree: neither remove nor rename files.
🔥@putout/operator-remove-files
Welcome new operator in a hood!
When you need to create a new plugin for RedLint that removes some kind of files just use:
import {operator} from 'putout';
const {removeFiles} = operator;
export const {
report,
fix,
scan,
} = removeFiles(['.DS_Store']);
It gives ability to write such rules as coverage/remove-files easily.
You still have ability to set files to you want to remove in .putout.json:
{
"match": {
".filesystem.json": {
"filesystem/remove-files": ["on", {
"names": ["coverage"]
}],
}
}
}
But also coverage/remove-files handles files related to coverage for you.
🔥@putout/operator-rename-properties
When you need to do some minor modifications to config files, like:
{
"rules": {
- "filesystem/remove-nyc-output": "off"
+ "coverage/remove-files": "off"
}
}
}
All you need is:
import {operator} from 'putout';
const {renameProperties} = operator;
export const {
report,
fix,
traverse,
} = renameProperties([
['filesystem/remove-nyc-output', 'coverage/remove-files'],
]);
Checkout in 🐊Putout Editor.
🔥@putout/operator-sort-ignore
When you need to sort things up
ignore
from:
node_modules
*.swp
yarn-error.log
yarn.lock
.idea
.DS_Store
deno.lock
coverage
.filesystem.json
to:
.idea
.filesystem.json
.DS_Store
*.swp
yarn-error.log
yarn.lock
deno.lock
node_modules
coverage
use:
import {operator} from 'putout';
const {sortIgnore} = operator;
export const {
report,
fix,
traverse,
} = sortIgnore({
name: '.gitignore',
});
json
And to sort up JSON:
{
"ignore": [
"**/package-lock.json",
"**/*.lock",
"**/.git",
"**/*.log",
"**/node_modules"
]
}
to
{
"ignore": [
"**/*.lock",
"**/*.log",
"**/.git",
"**/package-lock.json",
"**/node_modules"
],
}
Use:
import {operator} from 'putout';
const {sortIgnore, __json} = operator;
export const {
report,
fix,
traverse,
} = sortIgnore({
name: '.gitignore',
type: __json,
property: 'ignore',
});
🔥Totally ESM
🐊Putout now 100% ESM, since it is:
Linter that do not afraid to act like codemod
With help of:
You can migrate any kind of JavaScript codebase from CommonJS to ESM.
Here is the algorithm:
- Change
typeof yourpackage.jsonfromcommonjstomodule; - Run
putout --fix ., it will fix any code that usesrequireandmodule.exportstoimportandexport; - Run
redlint fix, it will resolve import names and changeimport {readFile} from './reader'toimport {readFile} from './reader.js'with help of Scanners introduced in Putout v34;
That's it!
If you avoid any kinds of hacks, like:
using mock-require:
❌ Example of incorrect code
// mock modules worked in CommonJS, but not in ESM
mockRequire('fs', {
readFile,
});
run(code);
✅ Example of correct code
// use dependency injection instead
run(code, {
readFile
});
using same names for exported and internal functions:
❌ Example of incorrect code
// 'run' already declared so we cannot just use 'const run = () => {run();}'
module.exports.run = () => {
return run();
};
function run() {
}
✅ Example of correct code
// easy to migrate to `export const run = () => {return runAll();}`, since no overlap with names
module.exports.run = () => {
return runAll();
};
function runAll() {
}
and reserved words
❌ Example of incorrect code
// 'delete is reserved word, you cannot use it to name variable `export const delete = () => {}`
module.exports.delete = () => {};
✅ Example of correct code
// it would be 'export const remove = () => {}`, so no problem at all
module.exports.remove = () => {}
If you check your code before migration, or a bit fix after migration to avoid such patterns, it will be much easier!
Any ways 🐊Putout always here to help you with any kind of your migrations 😏.
That's all for today, have a nice evening 🐈!
Top comments (0)