eslint-codemod-utils
Advanced tools
Comparing version 1.0.1 to 1.1.0
# eslint-codemod-utils | ||
## 1.1.0 | ||
### Minor Changes | ||
- 8913da9: Adds additional common utilties for codemod specific transforms. | ||
## 1.0.1 | ||
@@ -4,0 +10,0 @@ |
@@ -103,2 +103,14 @@ import type * as estree from 'estree-jsx'; | ||
export declare const jsxText: StringableASTNodeFn<estree.JSXText>; | ||
/** | ||
* __JSXEmptyExpression__ | ||
* | ||
* @example | ||
* | ||
* ```tsx | ||
* <SomeJSX attribute={} /> | ||
* ^^ | ||
* ``` | ||
* | ||
* @returns {estree.JSXEmptyExpression} | ||
*/ | ||
export declare const jsxEmptyExpression: StringableASTNodeFn<estree.JSXEmptyExpression>; | ||
@@ -105,0 +117,0 @@ export declare const jsxExpressionContainer: StringableASTNodeFn<estree.JSXExpressionContainer>; |
@@ -227,2 +227,14 @@ "use strict"; | ||
exports.jsxText = jsxText; | ||
/** | ||
* __JSXEmptyExpression__ | ||
* | ||
* @example | ||
* | ||
* ```tsx | ||
* <SomeJSX attribute={} /> | ||
* ^^ | ||
* ``` | ||
* | ||
* @returns {estree.JSXEmptyExpression} | ||
*/ | ||
const jsxEmptyExpression = (node) => { | ||
@@ -229,0 +241,0 @@ return { |
@@ -33,2 +33,23 @@ import type { ImportDeclaration, JSXElement } from 'estree-jsx'; | ||
/** | ||
* @example | ||
* ```tsx | ||
* insertImportDeclaration('source', ['specifier', 'second']) | ||
* | ||
* // produces | ||
* import { specifier, second } from 'source' | ||
* ``` | ||
* | ||
* @example | ||
* ```tsx | ||
* * insertImportDeclaration('source', ['specifier', { imported: 'second', local: 'other' }]) | ||
* | ||
* // produces | ||
* import { specifier, second as other } from 'source' | ||
* ``` | ||
*/ | ||
export declare function insertImportDeclaration(source: string, specifiers: (string | { | ||
local: string; | ||
imported: string; | ||
})[]): StringableASTNode<ImportDeclaration>; | ||
/** | ||
* Removes an import specifier to an existing import declaration. | ||
@@ -35,0 +56,0 @@ * |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.removeImportSpecifier = exports.insertImportSpecifier = exports.hasImportSpecifier = exports.hasImportDeclaration = exports.hasJSXChild = exports.hasJSXAttribute = exports.closestOfType = exports.isNodeOfType = void 0; | ||
exports.removeImportSpecifier = exports.insertImportDeclaration = exports.insertImportSpecifier = exports.hasImportSpecifier = exports.hasImportDeclaration = exports.hasJSXChild = exports.hasJSXAttribute = exports.closestOfType = exports.isNodeOfType = void 0; | ||
const nodes_1 = require("../nodes"); | ||
@@ -100,2 +100,37 @@ function isNodeOfType(node, type) { | ||
/** | ||
* @example | ||
* ```tsx | ||
* insertImportDeclaration('source', ['specifier', 'second']) | ||
* | ||
* // produces | ||
* import { specifier, second } from 'source' | ||
* ``` | ||
* | ||
* @example | ||
* ```tsx | ||
* * insertImportDeclaration('source', ['specifier', { imported: 'second', local: 'other' }]) | ||
* | ||
* // produces | ||
* import { specifier, second as other } from 'source' | ||
* ``` | ||
*/ | ||
function insertImportDeclaration(source, specifiers) { | ||
return (0, nodes_1.importDeclaration)({ | ||
source: (0, nodes_1.literal)(source), | ||
specifiers: specifiers.map((spec) => { | ||
return spec === 'default' | ||
? (0, nodes_1.importDefaultSpecifier)({ local: (0, nodes_1.identifier)('__default') }) | ||
: (0, nodes_1.importSpecifier)({ | ||
imported: typeof spec === 'string' | ||
? (0, nodes_1.identifier)(spec) | ||
: (0, nodes_1.identifier)(spec.imported), | ||
local: typeof spec === 'string' | ||
? (0, nodes_1.identifier)(spec) | ||
: (0, nodes_1.identifier)(spec.local), | ||
}); | ||
}), | ||
}); | ||
} | ||
exports.insertImportDeclaration = insertImportDeclaration; | ||
/** | ||
* Removes an import specifier to an existing import declaration. | ||
@@ -102,0 +137,0 @@ * |
@@ -276,2 +276,14 @@ import type * as estree from 'estree-jsx' | ||
/** | ||
* __JSXEmptyExpression__ | ||
* | ||
* @example | ||
* | ||
* ```tsx | ||
* <SomeJSX attribute={} /> | ||
* ^^ | ||
* ``` | ||
* | ||
* @returns {estree.JSXEmptyExpression} | ||
*/ | ||
export const jsxEmptyExpression: StringableASTNodeFn< | ||
@@ -278,0 +290,0 @@ estree.JSXEmptyExpression |
@@ -13,2 +13,3 @@ import type { | ||
importSpecifier, | ||
literal, | ||
} from '../nodes' | ||
@@ -159,2 +160,42 @@ import type { | ||
/** | ||
* @example | ||
* ```tsx | ||
* insertImportDeclaration('source', ['specifier', 'second']) | ||
* | ||
* // produces | ||
* import { specifier, second } from 'source' | ||
* ``` | ||
* | ||
* @example | ||
* ```tsx | ||
* * insertImportDeclaration('source', ['specifier', { imported: 'second', local: 'other' }]) | ||
* | ||
* // produces | ||
* import { specifier, second as other } from 'source' | ||
* ``` | ||
*/ | ||
export function insertImportDeclaration( | ||
source: string, | ||
specifiers: (string | { local: string; imported: string })[] | ||
): StringableASTNode<ImportDeclaration> { | ||
return importDeclaration({ | ||
source: literal(source), | ||
specifiers: specifiers.map((spec) => { | ||
return spec === 'default' | ||
? importDefaultSpecifier({ local: identifier('__default') }) | ||
: importSpecifier({ | ||
imported: | ||
typeof spec === 'string' | ||
? identifier(spec) | ||
: identifier(spec.imported), | ||
local: | ||
typeof spec === 'string' | ||
? identifier(spec) | ||
: identifier(spec.local), | ||
}) | ||
}), | ||
}) | ||
} | ||
/** | ||
* Removes an import specifier to an existing import declaration. | ||
@@ -161,0 +202,0 @@ * |
{ | ||
"name": "eslint-codemod-utils", | ||
"version": "1.0.1", | ||
"version": "1.1.0", | ||
"description": "A collection of AST helper functions for more complex ESLint rule fixes.", | ||
@@ -35,3 +35,3 @@ "source": "lib/index.ts", | ||
"typings": "dist/index.d.ts", | ||
"readme": "# ESLint Codemod Utilities\n\n<p align=\"center\">\n <a href=\"https://github.com/DarkPurple141/eslint-codemod-utils/actions/workflows/build-test.yml\">\n <img alt=\"Github Actions Build Status\" src=\"https://img.shields.io/github/workflow/status/DarkPurple141/eslint-codemod-utils/CI%20Build%20&%20Testing?style=flat-square\"></a>\n <a href=\"https://www.npmjs.com/package/eslint-codemod-utils\">\n <img alt=\"npm version\" src=\"https://img.shields.io/npm/v/eslint-codemod-utils?style=flat-square\"></a>\n</p>\n\nThe `eslint-codemod-utils` package is a library of helper functions designed to enable code evolution in a similar way to `jscodeshift` - but leaning on the live and ongoing enforcement of `eslint` in your source - rather than one off codemod scripts. It provides first class typescript support and will supercharge your custom eslint rules.\n\n## Installation\n\n```\npnpm add -D eslint-codemod-utils\n```\n\n```\nyarn add -D eslint-codemod-utils\n```\n\n```\nnpm i --save-dev eslint-codemod-utils\n```\n\n## Getting started\n\nTo create a basic JSX node, you might do something like this:\n\n```ts\nimport {\n jsxElement,\n jsxOpeningElement,\n jsxClosingElement,\n identifier,\n} from 'eslint-codemod-utils'\n\nconst modalName = identifier({ name: 'Modal' })\nconst modal = jsxElement({\n openingElement: jsxOpeningElement({ name: modalName, selfClosing: false }),\n closingElement: jsxClosingElement({ name: modalName }),\n})\n```\n\nThis would produce an `espree` compliant node type that you can **also** nicely stringify to apply your eslint\nfixes. For example:\n\n```ts\nmodal.toString()\n// produces: <Modal></Modal>\n```\n\nThe real power of this approach is when combining these utilties with `eslint` rule custom fixe. In these cases, rather than\nrelying on string manipulation - which can be inexact, hacky or complex to reason about - you can instead focus on only the fix you actually need to affect.\n\n### Your first `eslint` codemod\n\nWriting a codemod is generally broken down into three parts:\n\n1. Find\n2. Modify\n3. Remove / Cleanup\n\nThe `eslint` custom rule API allows us to find nodes fairly simply, but how might we modify them? Let's say we're trying to add a new element required to be composed by our Design System's Modal element - a `ModalBody` which is going to\nbe wrapped by the original `Modal` container. Assuming you've found the right node a normal fix might look like this:\n\n```ts\nimport { Rule } from 'eslint'\n\nfunction fix(fixer: Rule.RuleFixer) {\n return fixer.replaceText(node, '<Modal><ModalBody></ModalBody></Modal>')\n}\n```\n\nSo for this input:\n\n```ts\nconst MyModal = () => <Modal></Modal>\n```\n\nWe make this change:\n\n```diff\n- const MyModal = () => <Modal></Modal>\n+ const MyModal = () => <Modal><ModalBody></ModalBody></Modal>\n```\n\nThis kinda works, but the problem is the existing usage of Modal in our codebase is likely (guaranteed!) to be considerably more complex than\nthis example.\n\n- If our Modal has props, we need to consider them\n- If our Modal has children, we need to consider them\n- If our Modal is aliased, we need to consider that\n\nInstead of relying on string manipulation to reconstruct the existing AST, we instead leverage the information `eslint` is already giving to us.\n\n```ts\nimport * as esUtils from 'eslint-codemod-utils'\nimport { Rule } from 'eslint'\n\n// This is slightly more verbose, but it's considerably more robust -\n// Simply re-using and spitting out the exisitng AST as a string\nfunction fix(fixer: Rule.RuleFixer) {\n const jsxIdentifier = esUtils.jsxIdentifier({ name: 'ModalBody' })\n const modalBodyNode = esUtils.jsxElement({\n openingElement: esUtils.jsxOpeningElement({ name: jsxIdentifier }),\n closingElement: esUtils.jsxClosingElement({ name: jsxIdentifier }),\n // pass children of original element to new wrapper\n children: node.children,\n })\n return fixer.replaceText(\n node,\n esUtils.jsxElement({ ...node, children: [modalBodyNode] }).toString()\n )\n}\n```\n\nThe above will work for the original example:\n\n```diff\n- const MyModal = () => <Modal></Modal>\n+ const MyModal = () => <Modal><ModalBody></ModalBody></Modal>\n```\n\nBut it will also work for:\n\n```diff\n- const MyModal = () => <Modal type=\"full-width\"></Modal>\n+ const MyModal = () => <Modal type=\"full-width\"><ModalBody></ModalBody></Modal>\n```\n\nOr:\n\n```diff\n- const MyModal = () => <Modal><SomeChild/></Modal>\n+ const MyModal = () => <Modal><ModalBody><SomeChild/></ModalBody></Modal>\n```\n\nIt's a declarative approach to solve the same problem.\n\nSee the [eslint-plugin-example](packages/eslint-plugin-example) example for examples of more real world fixes.\n\n## How it works\n\nThe library provides a 1-1 mapping of types to utility functions every `espree` node type. These are all lowercase complements to the underlying type they represent;\neg. `jsxIdentifier` produces a `JSXIdentifier` node representation. These nodes all implement their own `toString`. This means any string cast will recursively produce the correct string output for any valid `espree` AST.\n\nEach helper takes in a valid `espree` node and spits out an augmented one that can be more easily stringified. See -> [API](API.md) for more.\n\n## Motivation\n\nThis idea came about after wrestling with the limitations of `eslint` rule fixes. For context, `eslint` rules rely heavily on string based utilities to apply\nfixes to code. For example this fix which appends a semi-colon to a `Literal` (from the `eslint` documentation website itself):\n\n```js\ncontext.report({\n node: node,\n message: 'Missing semicolon',\n fix: function (fixer) {\n return fixer.insertTextAfter(node, ';')\n },\n})\n```\n\nThis works fine if your fixes are trivial, but it works less well for more complex uses cases. As soon as you need to traverse other AST nodes and combine information for a fix, combine fixes; the simplicity of the `RuleFixer` API starts to buckle.\n\nIn codemod tools like [jscodeshift](https://github.com/facebook/jscodeshift), the AST is baked in to the way fixes are applied - rather than applying fixes your script needs to return a collection of AST nodes which are then parsed and integrated into the source. This is a little more heavy duty but it also is more resillient.\n\nThe missing piece for `ESlint` is a matching set of utilties to allow the flexibility to dive into the AST approach where and when a developer feels it is appropriate.\nThis library aims to bridge some of that gap and with some different thinking around just how powerful `ESLint` can be.\n\nFixes can then theoretically deal with more complex use cases like this:\n\n```ts\n/**\n * This is part of a fix to demonstrate changing a prop in a specific element with\n * a much more surgical approach to node manipulation.\n */\nimport {\n jsxOpeningElement,\n jsxAttribute,\n jsxIdentifier,\n} from 'eslint-codemod-utils'\n\n// ... further down the file\ncontext.report({\n node: node,\n message: 'error',\n fix(fixer) {\n // The variables 'fixed' works with the espree AST to create\n // its own representation which can easily be stringified\n const fixed = jsxOpeningElement({\n name: node.name,\n selfClosing: node.selfClosing,\n attributes: node.attributes.map((attr) => {\n if (attr.type === 'JSXAttribute' && attr.name.name === 'open') {\n const internal = jsxAttribute({\n // espree nodes are spread into the util with no issues\n ...attr,\n // others are recreated or re-mapped\n name: jsxIdentifier({\n ...attr.name,\n name: 'isOpen',\n }),\n })\n return internal\n }\n\n return attr\n }),\n })\n\n return fixer.replaceText(node, fixed.toString())\n },\n})\n```\n" | ||
"readme": "# ESLint Codemod Utilities\n\n<p align=\"center\">\n <a href=\"https://github.com/DarkPurple141/eslint-codemod-utils/actions/workflows/build-test.yml\">\n <img alt=\"Github Actions Build Status\" src=\"https://img.shields.io/github/workflow/status/DarkPurple141/eslint-codemod-utils/CI%20Build%20&%20Testing?style=flat-square\"></a>\n <a href=\"https://www.npmjs.com/package/eslint-codemod-utils\">\n <img alt=\"npm version\" src=\"https://img.shields.io/npm/v/eslint-codemod-utils?style=flat-square\"></a>\n</p>\n\nThe `eslint-codemod-utils` package is a library of helper functions designed to enable code evolution in a similar way to `jscodeshift` - but leaning on the live and ongoing enforcement of `eslint` in your source - rather than one off codemod scripts. It provides first class typescript support and will supercharge your custom eslint rules.\n\n## Installation\n\n```\npnpm add -D eslint-codemod-utils\n```\n\n```\nyarn add -D eslint-codemod-utils\n```\n\n```\nnpm i --save-dev eslint-codemod-utils\n```\n\n## Getting started\n\nTo create a basic JSX node, you might do something like this:\n\n```ts\nimport {\n jsxElement,\n jsxOpeningElement,\n jsxClosingElement,\n identifier,\n} from 'eslint-codemod-utils'\n\nconst modalName = identifier({ name: 'Modal' })\nconst modal = jsxElement({\n openingElement: jsxOpeningElement({ name: modalName, selfClosing: false }),\n closingElement: jsxClosingElement({ name: modalName }),\n})\n```\n\nThis would produce an `espree` compliant node type that you can **also** nicely stringify to apply your eslint\nfixes. For example:\n\n```ts\nmodal.toString()\n// produces: <Modal></Modal>\n```\n\nThe real power of this approach is when combining these utilties with `eslint` rule custom fixe. In these cases, rather than\nrelying on string manipulation - which can be inexact, hacky or complex to reason about - you can instead focus on only the fix you actually need to affect.\n\n### Your first `eslint` codemod\n\nWriting a codemod is generally broken down into three parts:\n\n1. Find\n2. Modify\n3. Remove / Cleanup\n\nThe `eslint` custom rule API allows us to find nodes fairly simply, but how might we modify them? Let's say we're trying to add a new element required to be composed by our Design System's Modal element - a `ModalBody` which is going to\nbe wrapped by the original `Modal` container. Assuming you've found the right node a normal fix might look like this:\n\n```ts\nimport { Rule } from 'eslint'\n\nfunction fix(fixer: Rule.RuleFixer) {\n return fixer.replaceText(node, '<Modal><ModalBody></ModalBody></Modal>')\n}\n```\n\nSo for this input:\n\n```ts\nconst MyModal = () => <Modal></Modal>\n```\n\nWe make this change:\n\n```diff\n- const MyModal = () => <Modal></Modal>\n+ const MyModal = () => <Modal><ModalBody></ModalBody></Modal>\n```\n\nThis kinda works, but the problem is the existing usage of Modal in our codebase is likely (guaranteed!) to be considerably more complex than\nthis example.\n\n- If our Modal has props, we need to consider them\n- If our Modal has children, we need to consider them\n- If our Modal is aliased, we need to consider that\n\nInstead of relying on string manipulation to reconstruct the existing AST, we instead leverage the information `eslint` is already giving to us.\n\n```ts\nimport * as esUtils from 'eslint-codemod-utils'\nimport { Rule } from 'eslint'\n\n// This is slightly more verbose, but it's considerably more robust -\n// Simply re-using and spitting out the exisitng AST as a string\nfunction fix(fixer: Rule.RuleFixer) {\n const jsxIdentifier = esUtils.jsxIdentifier({ name: 'ModalBody' })\n const modalBodyNode = esUtils.jsxElement({\n openingElement: esUtils.jsxOpeningElement({ name: jsxIdentifier }),\n closingElement: esUtils.jsxClosingElement({ name: jsxIdentifier }),\n // pass children of original element to new wrapper\n children: node.children,\n })\n return fixer.replaceText(\n node,\n esUtils.jsxElement({ ...node, children: [modalBodyNode] }).toString()\n )\n}\n```\n\nThe above will work for the original example:\n\n```diff\n- const MyModal = () => <Modal></Modal>\n+ const MyModal = () => <Modal><ModalBody></ModalBody></Modal>\n```\n\nBut it will also work for:\n\n```diff\n- const MyModal = () => <Modal type=\"full-width\"></Modal>\n+ const MyModal = () => <Modal type=\"full-width\"><ModalBody></ModalBody></Modal>\n```\n\nOr:\n\n```diff\n- const MyModal = () => <Modal><SomeChild/></Modal>\n+ const MyModal = () => <Modal><ModalBody><SomeChild/></ModalBody></Modal>\n```\n\nIt's a declarative approach to solve the same problem.\n\nSee the [eslint-plugin-example](packages/eslint-plugin-codemod) example for examples of more real world fixes.\n\n## How it works\n\nThe library provides a 1-1 mapping of types to utility functions every `espree` node type. These are all lowercase complements to the underlying type they represent;\neg. `jsxIdentifier` produces a `JSXIdentifier` node representation. These nodes all implement their own `toString`. This means any string cast will recursively produce the correct string output for any valid `espree` AST.\n\nEach helper takes in a valid `espree` node and spits out an augmented one that can be more easily stringified. See -> [API](API.md) for more.\n\n## Motivation\n\nThis idea came about after wrestling with the limitations of `eslint` rule fixes. For context, `eslint` rules rely heavily on string based utilities to apply\nfixes to code. For example this fix which appends a semi-colon to a `Literal` (from the `eslint` documentation website itself):\n\n```js\ncontext.report({\n node: node,\n message: 'Missing semicolon',\n fix: function (fixer) {\n return fixer.insertTextAfter(node, ';')\n },\n})\n```\n\nThis works fine if your fixes are trivial, but it works less well for more complex uses cases. As soon as you need to traverse other AST nodes and combine information for a fix, combine fixes; the simplicity of the `RuleFixer` API starts to buckle.\n\nIn codemod tools like [jscodeshift](https://github.com/facebook/jscodeshift), the AST is baked in to the way fixes are applied - rather than applying fixes your script needs to return a collection of AST nodes which are then parsed and integrated into the source. This is a little more heavy duty but it also is more resillient.\n\nThe missing piece for `ESlint` is a matching set of utilties to allow the flexibility to dive into the AST approach where and when a developer feels it is appropriate.\nThis library aims to bridge some of that gap and with some different thinking around just how powerful `ESLint` can be.\n\nFixes can then theoretically deal with more complex use cases like this:\n\n```ts\n/**\n * This is part of a fix to demonstrate changing a prop in a specific element with\n * a much more surgical approach to node manipulation.\n */\nimport {\n jsxOpeningElement,\n jsxAttribute,\n jsxIdentifier,\n} from 'eslint-codemod-utils'\n\n// ... further down the file\ncontext.report({\n node: node,\n message: 'error',\n fix(fixer) {\n // The variables 'fixed' works with the espree AST to create\n // its own representation which can easily be stringified\n const fixed = jsxOpeningElement({\n name: node.name,\n selfClosing: node.selfClosing,\n attributes: node.attributes.map((attr) => {\n if (attr.type === 'JSXAttribute' && attr.name.name === 'open') {\n const internal = jsxAttribute({\n // espree nodes are spread into the util with no issues\n ...attr,\n // others are recreated or re-mapped\n name: jsxIdentifier({\n ...attr.name,\n name: 'isOpen',\n }),\n })\n return internal\n }\n\n return attr\n }),\n })\n\n return fixer.replaceText(node, fixed.toString())\n },\n})\n```\n" | ||
} |
@@ -141,3 +141,3 @@ # ESLint Codemod Utilities | ||
See the [eslint-plugin-example](packages/eslint-plugin-example) example for examples of more real world fixes. | ||
See the [eslint-plugin-example](packages/eslint-plugin-codemod) example for examples of more real world fixes. | ||
@@ -144,0 +144,0 @@ ## How it works |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
689173
45
21044