- Notifications
You must be signed in to change notification settings - Fork6
Super powerful structural search and replace for JavaScript and TypeScript to automate your refactoring
License
codemodsquad/astx
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Super powerful structural search and replace for JavaScript and TypeScript to automate your refactoring
- astx
- Table of Contents
- Introduction
- Usage examples
- Roadmap
- VSCode Extension
- Prior art and philosophy
- Pattern Language
- API
- interface NodePath
- class Astx
constructor(backend: Backend, paths: NodePath<any>[] | Match[], options?: { withCaptures?: Match[] })
.find(...)
(Astx
).closest(...)
(Astx
).destruct(...)
(Astx
)FindOptions
.find(...).replace(...)
(void
).findImports(...)
(Astx
).addImports(...)
(Astx
).removeImports(...)
(boolean
).replaceImport(...).with(...)
(boolean
).remove()
(void
).matched
(this | null
).size()
(number
)[name: `$${string}` | `$$${string}` | `$$$${string}`]
(Astx
).placeholder
(string | undefined
).node
(Node
).path
(NodePath
).code
(string
).stringValue
(string
)[Symbol.iterator]
(Iterator<Astx>
).matches
(Match[]
).match
(Match
).paths
(NodePath[]
).nodes
(Node[]
).some(predicate)
(boolean
).every(predicate)
(boolean
).filter(iteratee)
(Astx
).map<T>(iteratee)
(T[]
).at(index)
(Astx
).withCaptures(...captures)
(Astx
)
- Match
- Transform files
- Configuration
- CLI
Simple refactors can be tedious and repetitive. For example say you want to make the following changeacross a codebase:
// before:rmdir('old/stuff')rmdir('new/stuff',true)// after:rmdir('old/stuff')rmdir('new/stuff',{force:true})
Changing a bunch of calls tormdir
by hand would suck. You could try using regex replace, but it's fiddly and wouldn't tolerate whitespace andlinebreaks well unless you work really hard at the regex. You could even usejscodeshift
, but it takes too long for simple cases like this, and starts to feel harder than necessary...
Now there's a better option...you can refactor with confidence usingastx
!
astx \ --find'rmdir($path, $force)' \ --replace'rmdir($path, { force: $force })'
This is a basic example ofastx
patterns, which are just JS or TS code that can containplaceholders and otherspecial matching constructs;astx
looks forcode matching the pattern, accepting any expression in place of$path
and$force
. Thenastx
replaceseach match with the replace pattern, substituting the expressions it captured for$path
('new/stuff'
) and$force
(true
).
But this is just the beginning;astx
patterns can be much more complex and powerful than this, and for really advanced use cases it hasan intuitiveAPI you can use:
for(constmatchofastx.find`rmdir($path, $force)`){const{ $path, $force}=match// do stuff with $path.node, $path.code, etc...}
Got a lot ofDo not access Object.prototype method 'hasOwnProperty' from target object
errors?
// astx.jsexports.find=`$a.hasOwnProperty($b)`exports.replace=`Object.hasOwn($a, $b)`
Recently for work I wanted to make this change:
// beforeconstpkg=OrgPackage({subPackage:['services',async ?'async' :'blocking', ...namespace,Names.ServiceType({ resource, async}),],})// afterconstpkg=[ ...OrgPackage(),'services',async ?'async' :'blocking', ...namespace,Names.ServiceType({ resource, async}),]
This is simple to do withlist matching:
// astx.jsexports.find=`OrgPackage({subPackage: [$$p]})`exports.replace=`[...OrgPackage(), $$p]`
// astx.jsexports.find=`const $id = require('$source')`exports.replace=`import $id from '$source'`
// astx.jsexportconstfind=`if ($a) { if ($b) $body }`exportconstreplace=`if ($a && $b) $body`
Injscodeshift-add-imports
I had a bunch of test cases following this pattern:
it(`leaves existing default imports untouched`,function(){constcode=`import Baz from 'baz'`constroot=j(code)constresult=addImports(root,statement`import Foo from 'baz'`)expect(result).to.deep.equal({Foo:'Baz'})expect(root.toSource()).to.equal(code)})
I wanted to make them more DRY, like this:
it(`leaves existing default imports untouched`,function(){testCase({code:`import Baz from 'baz'`,add:`import Foo from 'baz'`,expectedCode:`import Baz from 'baz'`,expectedReturn:{Foo:'Baz'},})})
Here was a transform for the above. (Of course, I had to run a few variations of this forcases where the expected code was different, etc.)
exports.find=`const code = $codeconst root = j(code)const result = addImports(root, statement\`$add\`)expect(result).to.deep.equal($expectedReturn)expect(root.toSource()).to.equal(code)`exports.replace=`testCase({ code: $code, add: \`$add\`, expectedCode: $code, expectedReturn: $expectedReturn,})`
I just finally got version 2 released as of December 2022 after tons of hard work 🎉Right now I'm working on theVSCode Extension.After that I want to make a documentation website that better illustrates how to useastx
.
TheVSCode Extension is currently in beta, but try it out!
While I was thinking about making this I discoveredgrasp, a similar tool that inspired the$
capture syntax.There are several reasons I decided to makeastx
anyway:
- Grasp uses the Acorn parser, which doesn't support TypeScript or Flow code AFAIK
- Hasn't been updated in 4 years
- Grasp's replace pattern syntax is clunkier, doesn't match the find pattern syntax:
grasp -e 'setValue($k, $v, true)' -R 'setValueSilently({{k}}, {{v}})' file.js
- It has its own DSL (SQuery) that's pretty limited and has a slight learning curve
- I wanted a
jscodeshift
-like API I could use in JS for advanced use cases that are probably awkward/impossible in Grasp
So the philosophy ofastx
is:
- Provide a simple find and replace pattern syntax that's ideal for simple cases and has minimum learning curve
- Use the same search and replace pattern syntax in the javascript API for anything more complex, so that you have unlimited flexibility
Paste your code intoAST Explorer if you need to learn about the structure of the AST.
Astx find patterns are just JavaScript or TypeScript code that may containplaceholder wildcards or other special constructslike$Or(A, B)
. Generally speaking, parts of the pattern that aren't wildcards or special constructs have to match exactly.
For example, the find patternfoo($a)
matches any call to the functionfoo
with a single argument. The argument can anythingand iscaptured as$a
.
Replace patterns are almost identical to find patterns, except that placeholders get replaced with whatever wascaptured intothe placeholder name by the find pattern, and special find constructs like$Or(A, B)
have no special meaning in replace patterns.(In the future, there may be special replace constructs that perform some kind of transformation on captured nodes.)
For example, the find patternfoo($a)
matchesfoo(1 + 2)
, then the replace patternfoo({ value: $a })
will generate the codefoo({ value: 1 + 2 })
.
Generally speaking, an identifier starting with$
is aplaceholder that functions like a wildcard. There are three types of placeholders:
$<name>
matches any single node ("node placeholder")$$<name>
matches a contiguous list of nodes ("array placeholder")$$$<name>
: matches all other siblings ("rest placeholder")
The<name>
(if given) must start with a letter or number; otherwise the identifier willnot be treated as a placeholder.
Rest placeholders ($$$
) may not be sibilings of ordered list placeholders ($$
).
Unless a placeholder is anonymous, it will "capture" the matched node(s), meaning you can use the same placeholder in thereplacement pattern to interpolate the matched node(s) into the generated replacement. In the Node API you can also accessthe captured AST paths/nodes via the placeholder name.
These placeholders match a single node. For example, the pattern[$a, $b]
matches an array expression with two elements,and those elements are captured as$a
and$b
.
These placeholders match a contiguous list of nodes. For example, the pattern[1, $$a, 2, $$b]
matches an array expressionwith1
as the first element, and2
as a succeeding element. Any elements between1
and the first2
is captured as$$a
,and elements after the first2
are captured as$$b
.
These placeholders match the rest of the siblings that weren't matched by something else. For example, the pattern[1, $$$a, 2]
matches an array expression that has elements1
and2
at any index. Any other elements (including additional occurrences of1
and2
) are captured as$$$a
.
You can use a placeholder without a name to match node(s) without capturing them.$
will match any single node,$$
will match acontiguous list of nodes, and$$$
will match all other siblings.
If you use the same capture placeholder more than once, subsequent positions will have to match what was captured for the first occurrence of the placeholder.
For example, the patternfoo($a, $a, $b, $b)
will match onlyfoo(1, 1, {foo: 1}, {foo: 1})
in the following:
foo(1,1,{foo:1},{foo:1})// matchfoo(1,2,{foo:1},{foo:1})// no matchfoo(1,1,{foo:1},{bar:1})// no match
Note: array capture placeholders ($$a
) and rest capture placeholders ($$$a
) don't currently support backreferencing.
AnObjectExpression
(aka object literal) pattern will match anyObjectExpression
in your code with the same properties in any order.It will not match if there are missing or additional properties. For example,{ foo: 1, bar: $bar }
will match{ foo: 1, bar: 2 }
or{ bar: 'hello', foo: 1 }
but not{ foo: 1 }
or{ foo: 1, bar: 2, baz: 3 }
.
You can match additional properties by using...$$captureName
, for example{ foo: 1, ...$$rest }
will match{ foo: 1 }
,{ foo: 1, bar: 2 }
,{ foo: 1, bar: 2, ...props }
etc.The additional properties will be captured inmatch.arrayCaptures
/match.arrayPathCaptures
, and can be spread in replacement expressions. For example,astx.find`{ foo: 1, ...$$rest }`.replace`{ bar: 1, ...$$rest }`
will transform{ foo: 1, qux: {}, ...props }
into{ bar: 1, qux: {}, ...props }
.
A spread property that isn't of the form/^\$\$[a-z0-9]+$/i
is not a capture placeholder, for example{ ...foo }
will only match{ ...foo }
and{ ...$_$foo }
will onlymatch{ ...$$foo }
(leading$_
is an escape for$
).
There is currently no way to match properties in a specific order, but it could be added in the future.
In many cases where there is a list of nodes in the AST you can matchmultiple elements with a placeholder starting with$$
. For example,[$$before, 3, $$after]
will match any array expression containing an element3
; elements before thefirst3
will be captured in$$before
and elements after the first3
will be captured in$$after
.
This works even with block statements. For example,function foo() { $$before; throw new Error('test'); $$after; }
will matchfunction foo()
that contains athrow new Error('test')
,and the statements before and after that throw statement will get captured in$$before
and$$after
, respectively.
In some cases list matching will be ordered by default, and in some cases it will be unordered. For example,ObjectExpression
property matches are unordered by default, as shown in the table below.Using a$$
placeholder or the special$Ordered
placeholder will force ordered matching. Using a$$$
placeholder or the special$Unordered
placeholder will force unordered matching.
If you use a placeholder starting with$$$
, it's treated as a "rest" capture, and all other elements of thematch expression will be matched out of order. For example,import {a, b, $$$rest} from 'foo'
would matchimport {c, b, d, e, a} from 'foo'
, putting specifiersc
,d
, ande
, into the$$$rest
placeholder.
Rest placeholders ($$$
) may not be sibilings of ordered list placeholders ($$
).
Some items marked TODO probably actually work, but are untested.
Type | Supports list matching? | Unordered by default? | Notes |
---|---|---|---|
ArrayExpression.elements | ✅ | ||
ArrayPattern.elements | ✅ | ||
BlockStatement.body | ✅ | ||
CallExpression.arguments | ✅ | ||
Class(Declaration/Expression).implements | ✅ | ✅ | |
ClassBody.body | ✅ | ✅ | |
ComprehensionExpression.blocks | TODO | ||
DeclareClass.body | TODO | ✅ | |
DeclareClass.implements | TODO | ✅ | |
DeclareExportDeclaration.specifiers | TODO | ✅ | |
DeclareInterface.body | TODO | ||
DeclareInterface.extends | TODO | ||
DoExpression.body | TODO | ||
ExportNamedDeclaration.specifiers | ✅ | ✅ | |
Function.decorators | TODO | ||
Function.params | ✅ | ||
FunctionTypeAnnotation/TSFunctionType.params | ✅ | ||
GeneratorExpression.blocks | TODO | ||
ImportDeclaration.specifiers | ✅ | ✅ | |
(TS)InterfaceDeclaration.body | TODO | ✅ | |
(TS)InterfaceDeclaration.extends | TODO | ✅ | |
IntersectionTypeAnnotation/TSIntersectionType.types | ✅ | ✅ | |
JSX(Element/Fragment).children | ✅ | ||
JSX(Opening)Element.attributes | ✅ | ✅ | |
MethodDefinition.decorators | TODO | ||
NewExpression.arguments | ✅ | ||
ObjectExpression.properties | ✅ | ✅ | |
ObjectPattern.decorators | TODO | ||
ObjectPattern.properties | ✅ | ✅ | |
(ObjectTypeAnnotation/TSTypeLiteral).properties | ✅ | ✅ | Use$a: $ to match one property,$$a: $ or$$$a: $ to match multiple |
Program.body | ✅ | ||
Property.decorators | TODO | ||
SequenceExpression | ✅ | ||
SwitchCase.consequent | ✅ | ||
SwitchStatement.cases | TODO | ||
TemplateLiteral.quasis/expressions | ❓ not sure if I can come up with a syntax | ||
TryStatement.guardedHandlers | TODO | ||
TryStatement.handlers | TODO | ||
TSFunctionType.parameters | ✅ | ||
TSCallSignatureDeclaration.parameters | TODO | ||
TSConstructorType.parameters | TODO | ||
TSConstructSignatureDeclaration.parameters | TODO | ||
TSDeclareFunction.params | TODO | ||
TSDeclareMethod.params | TODO | ||
EnumDeclaration.body/TSEnumDeclaration.members | TODO | ✅ | |
TSIndexSignature.parameters | TODO | ||
TSMethodSignature.parameters | TODO | ||
TSModuleBlock.body | TODO | ||
TSTypeLiteral.members | ✅ | ✅ | |
TupleTypeAnnotation/TSTupleType.types | ✅ | ||
(TS)TypeParameterDeclaration.params | ✅ | ||
(TS)TypeParameterInstantiation.params | ✅ | ||
UnionTypeAnnotation/TSUnionType.types | ✅ | ✅ | |
VariableDeclaration.declarations | ✅ | ||
WithStatement.body | ❌ who uses with statements... |
A string that's just a placeholder like'$foo'
will match any string and capture its contents intomatch.stringCaptures.$foo
.The same escaping rules apply as for identifiers. This also works for template literals like`$foo`
and tagged template literals likedoSomething`$foo`
.
This can be helpful for working with import statements. For example, seeConverting require statements to imports.
An empty comment (/**/
) in a pattern will "extract" a node for matching.For example the patternconst x = { /**/ $key: $value }
will justmatchObjectProperty
nodes against$key: $value
.
The parser wouldn't be able to parse$key: $value
by itself orknow that you mean anObjectProperty
, as opposed to something different like thex: number
inconst x: number = 1
, so using/**/
enables you to work around this. You can use this to match any node type that isn't a valid expression or statement by itself. For exampletype T = /**/ Array<number>
would matchArray<number>
type annotations.
/**/
also works in replacement patterns.
Matches either the given expression or no node in its place. For examplelet $a = $Maybe(2)
will matchlet foo = 2
andlet foo
(with no initializer), but notlet foo = 3
.
Matches nodes that match at least one of the given patterns. For example$Or(foo($$args), {a: $value})
will match calls tofoo
and object literals with only ana
property.
Matches nodes that match all of the given patterns. This is mostly useful for narrowing down the types of nodes that can be captured into a given placeholder. For example,let $a = $And($init, $a + $b)
will matchlet
declarations where the initializer matches$a + $b
, and capture the initializer as$init
.
Matches either the given type annotation or no node in its place. For examplelet $a: $Maybe<number>
will matchlet foo: number
andlet foo
(with no type annotation), but notlet foo: string``let foo: string
.
Matches nodes that match at least one of the given type annotations. For examplelet $x: $Or<number[], string[]>
will matchlet
declarations of typenumber[]
orstring[]
.
Matches nodes that match all of the given type annotations. This is mostly useful for narrowing down the types of nodes that can be captured into a given placeholder. For example,let $a: $And<$type, $elem[]>
will matchlet
declarations where the type annotation matches$elem[]
, and capture the type annotation as$type
.
Forces the pattern to match sibling nodes in the same order.
Forces the pattern to match sibling nodes in any order.
import{NodePath}from'astx'
This is the sameNodePath
interface asast-types
, with some improvements to the method type definitions.astx
usesast-types
to traverse code, in hopes of supporting different parsers in the future.
import{Astx}from'astx'
constructor(backend: Backend, paths: NodePath<any>[] | Match[], options?: { withCaptures?: Match[] })
backend
is the parser/generator implementation being used.
paths
specifies theNodePath
s orMatch
es you wantAstx
methodsto search/operate on.
Finds matches for the given pattern within this instance's starting paths and returns anAstx
instance containing the matches.
If you callastx.find('foo($$args)')
on the initial instance passed to your transform function, it will find all calls tofoo
within the file, and return those matches in a newAstx
instance.
Methods on the returned instance will operate only on the matched paths.
For example if you doastx.find('foo($$args)').find('$a + $b')
, the secondfind
call will only search for$a + $b
within matches tofoo($$args)
, rather than anywhere in the file.
You can call.find
as a method or tagged template literal:
.find`pattern`
.find(pattern: string | string[] | Node | Node[] | NodePath | NodePath[] | ((wrapper: Astx) => boolean), options?: FindOptions)
If you give the pattern as a string, it must be a valid expression or statement(s). Otherwise it should be validAST node(s) you already parsed or constructed.You can interpolate strings, AST nodes, arrays of AST nodes, andAstx
instances in the tagged template literal.
For example you could doastx.find`${t.identifier('foo')} + 3`
.
Or you could match multiple statements by doing
astx.find` const $a = $b; $$c; const $d = $a + $e;`
This would match (for example) the statementsconst foo = 1; const bar = foo + 5;
, with any number of statements between them.
Like.find()
, but searches up the AST ancestors instead of down into descendants; finds the closest enclosing node of each input path that matches the given pattern.
Like.find()
, but doesn't test descendants of the input path(s) against the pattern; only input paths matching the pattern will be includedin the result.
An object with the following optional properties:
Where conditions for node captures. For example if your find pattern is$a()
, you could have{ where: { $a: astx => /foo|bar/.test(astx.node.name) } }
, which would only match zero-argument callstofoo
orbar
.
Finds and replaces matches for the given pattern withinroot
.
There are several different ways you can call.replace
. You can call.find
in any way described above.
.find(...).replace`replacement`
.find(...).replace(replacement: string | string | Node | Node[])
.find(...).replace(replacement: (match: Astx, parse: ParsePattern) => string)
.find(...).replace(replacement: (match: Astx, parse: ParsePattern) => Node | Node[])
If you give the replacement as a string, it must be a valid expression or statement.You can give the replacement as AST node(s) you already parsed or constructed.Or you can give a replacement function, which will be called with each match and must return a string orNode | Node[]
(you can use theparse
tagged template string function provided as the second argument to parse code into a string.For example, you could uppercase the function names in all zero-argument function calls (foo(); bar()
becomesFOO(); BAR()
) with this:
astx .find`$fn()` .replace(({ captures: { $fn } }) => `${$fn.name.toUpperCase()}()`)
A convenience version of.find()
for finding imports that tolerates extra specifiers,matches value imports of the same name if type imports were requested, etc.
For example.findImports`import $a from 'a'`
would matchimport A, { b, c } from 'a'
orimport { default as a } from 'a'
, capturing$a
, whereas.find`import $a from 'a'`
would not match either of these cases.
The pattern must contain only import statements.
Like.findImports()
, but adds any imports that were not found. For example given thesource code:
import{foo,typebarasqux}from'foo'import'g'
And the operation
const{ $bar}=astx.addImports` import type { bar as $bar } from 'foo' import FooDefault from 'foo' import * as g from 'g'`
The output would be
importFooDefault,{foo,typebarasqux}from'foo'import*asgfrom'g'
With$bar
capturing the identifierqux
.
Takes import statements in the same format as.findImports()
but removes all given specifiers.
Replaces a single import specifier with another. For example given the input
import{Match,Route,Location}from'react-router-dom'importtype{History}from'history'
And operation
astx.replaceImport` import { Location } from 'react-router-dom'`.with` import type { Location } from 'history'`
The output would be
import{Match,Route}from'react-router-dom'importtype{History,Location}from'history'
The find and replace patterns must both contain a single import statementwith a single specifier.
Removes the matches from.find()
or focused capture(s) in thisAstx
instance.
Returns thisAstx
instance if it has at least one match, otherwise returnsnull
.
Since.find()
,.closest()
, and.destruct()
always return anAstx
instance, even if there were nomatches, you can use.find(...).matched
if you only want a defined value when there was at leastone match.
Returns the number of matches from the.find()
or.closest()
call that returned this instance.
Gets anAstx
instance focused on the capture(s) with the givenname
.
For example, you can do:
for(const{ $v}ofastx.find`process.env.$v`){report($v.code)}
The name of the placeholder this instance represents. For example:
constmatch=astx.find`function $fn($$params) { $$body }`console.log(match.placeholder)// undefinedconst{ $fn, $$params}=matchconsole.log($fn.placeholder)// $fnconsole.log($$params.placeholder)// $$params
Returns the first node of the first match. Throws an error if there are no matches.
Returns the first path of the first match. Throws an error if there are no matches.
Generates code from the first node of the first match. Throws an error if there are no matches.
Returns the string value of the first node if the focused capture is a string capture. Throws an error if there are no matches.
Iterates through each match, returning anAstx
instance for each match.
Gets the matches from the.find()
or.closest()
call that returned this instance.
Gets the first match from the.find()
or.closest()
call that returned this instance.
Throws an error if there were no matches.
Returns the paths that.find()
and.closest()
will search within.If this instance was returned by.find()
or.closest()
, these arethe paths of nodes that matched the search pattern.
Returns the nodes that.find()
and.closest()
will search within.If this instance was returned by.find()
or.closest()
, these arethe nodes that matched the search pattern.
Returnsfalse
unlesspredicate
returns truthy for at least one match.
iteratee
is function that will be called withmatch: Astx, index: number, parent: Astx
and returnstrue
orfalse
.
Returnstrue
unelsspredicate
returns falsy for at least one match.
iteratee
is function that will be called withmatch: Astx, index: number, parent: Astx
and returnstrue
orfalse
.
Filters the matches.
iteratee
is function that will be called withmatch: Astx, index: number, parent: Astx
and returnstrue
orfalse
. Only matches for whichiteratee
returnstrue
will be included in the result.
Maps the matches.
iteratee
is function that will be called withmatch: Astx, index: number, parent: Astx
and returns the value to includein the result array.
Selects the match at the givenindex
.
Returns anAstx
instance that contains captures from the given...captures
in addition to captures present in this instance.
You can pass the following kinds of arguments:
Astx
instances - all captures from the instance will be included.Astx[placeholder]
instances - capture(s) for the givenplaceholder
will be included.{ $name: Astx[placeholder] }
- capture(s) for the givenplaceholder
, renamed to$name
.Match
objects
import{typeMatch}from'astx'
The type of match:'node'
or'nodes'
.
TheNodePath
of the matched node. Iftype
is'nodes'
, this will bepaths[0]
.
The matchedNode
. Iftype
is'nodes'
, this will benodes[0]
.
TheNodePaths
of the matched nodes.
The matchedNode
s.
TheNode
s captured from placeholders in the match pattern. For example if the pattern wasfoo($bar)
,.captures.$bar
will be theNode
of the first argument.
TheNodePath
s captured from placeholders in the match pattern. For example if the pattern wasfoo($bar)
,.pathCaptures.$bar
will be theNodePath
of the first argument.
TheNode[]
s captured from array placeholders in the match pattern. For example if the pattern wasfoo({ ...$bar })
,.arrayCaptures.$bar
will be theNode[]
s of the object properties.
TheNodePath[]
s captured from array placeholders in the match pattern. For example if the pattern wasfoo({ ...$bar })
,.pathArrayCaptures.$bar
will be theNodePath[]
s of the object properties.
The string values captured from string placeholders in the matchpattern. For example if the pattern wasimport foo from '$foo'
,stringCaptures.$foo
will be the import path.
Likejscodeshift
, you can put code to perform a transform in a.ts
or.js
file (defaults toastx.ts
orastx.js
in the working directory, unless you specify a different file with the-t
CLI option).
The transform file API is a bit different fromjscodeshift
though. You can have the following exports:
A code string or AST node of the pattern to find in the files being transformed.
Where conditions for capture placeholders inexports.find
.SeeFindOptions.where
({ [captureName: string]: (path: NodePath<any>) => boolean }
) for more information.
A code string, AST node, or replace function to replace matches ofexports.find
with.
The function arguments are the same as described in.find().replace()
.
A function to perform an arbitrary transform using theAstx
API. It gets called with an object with the following properties:
file
(string
) - The path to the file being transformedsource
(string
) - The source code of the file being transformedastx
(Astx
) - theAstx
API instancet
(AstTypes
) -ast-types
definitions for the chosen parserexpression
- tagged template literal for parsing code as an expressionstatement
- tagged template literal for parsing code as a statementstatements
- tagged template literal for parsing code as an array of statementsreport
((message: unknown) => void
)mark
((...matches: Astx[]) => void
) - marks the given matches to be displayed in the matches list of vscode-astx, etc
Unlikejscodeshift
, your transform function can be async, and it doesn't have to return the transformed code,but you can return astring
. You can also returnnull
toskip the file.
If your callreport(x)
from anexports.astx
function, this will be called withonReport({ file, report: x })
.
If you are using multiple worker threads,onReport
will be called in the parent process, so the reportmessage must be a serializable value. This allows atransform to collect reports from all workers (and thenpotentially do something with them infinish
).
IfonReport
returns aPromise
it will be awaited.
This will be called after the transform has been run onall input files.
If you are using multiple worker threads,finish
will be called in the parent process. You can useonReport
andfinish
together to collect information from each input fileand produce some kind of combined output at the end.
Iffinish
returns aPromise
it will be awaited.
astx
supports configuration in the following places (viacosmiconfig
):
- an
astx
property in package.json - an
.astxrc
file in JSON or YAML format - an
.astxrc.json
,.astxrc.yaml
,.astxrc.yml
,.astxrc.js
, or.astxrc.cjs
file - an
astx.config.js
orastx.config.cjs
CommonJS module exporting an object
If your codebase is formatted with prettier, I recommend trying this first:
{"parser":"babel/auto","parserOptions": {"preserveFormat":"generatorHack" }}
(or as CLI options)
--parser babel/auto --parserOptions '{"preserveFormat": "generatorHack"}'
If this fails you can tryparser: 'recast/babel/auto'
or the non-/auto
parsers.
Your mileage may vary withrecast
; they just aren't able to keep it up to datewith new syntax features in JS and TS quickly enough, and I've seen it output invalidsyntax too many times.
From now on I'm going to work on a reliable solution using@babel/generator
orprettier
to print the modified AST, with a hook to use the original source verbatim for unmodifiednodes.
The parser to use. Options:
babel/auto
(default,)babel
(faster thanbabel/auto
, but uses default parse options instead, you may have to configureparserOptions
)recast/babel
recast/babel/auto
babel/auto
automatically determines parse options from your babel config if present.babel
uses fixed parse options instead, so it's faster thanbabel/auto
, but you may have to configureparserOptions
.Therecast/babel(/auto)
options userecast
to preserve formatting.I've seenrecast
output invalid syntax on some files, so use with caution.
Options to pass to the parser. Right now this is just the@babel/parser
options plusthe following additional options:
preserveFormat
(applies to:babel
,babel/auto
)preserveFormat: 'generatorHack'
uses an experimental hackto preserve format of all unchanged nodes by hijackinginternal@babel/generator
API.
Iffalse
, don't try to useprettier
to reformat transformed source code.Defaults totrue
.
Astx includes a CLI for performing transforms. The CLI will process the given files, then print out a diff of what will bechanged, and prompt you to confirm you want to write the changes.
It will parse with babel by default using the version installed in your project and your project's babel config, if any.You can pass--parser recast/babel
if you want to userecast
to try to preserveformatting in the output, but I sometimes see syntax errors in its output.
Unlikejscodeshift
, ifprettier
is installed in your project, it will format the transformed code withprettier
.
Usage:astx -f <code> [<files...>] [<directories...>] Searches for the -f pattern in the given files and directories and prints out the matches in contextastx -f <code> -r <code> [<files...>] [<directories...>] Quick search and replace in the given files and directories (make sure to quote code) Example: astx -f 'rmdir($path, $force)' -r 'rmdir($path, { force: $force })' srcastx -t <transformFile> [<files ...>] [<directories ...>] Applies a transform file to the given files and directoriesastx [<files ...>] [<directories ...>] Applies the default transform file (astx.ts or astx.js in working directory) to the given files and directoriesOptions: --help Show help [boolean] --version Show version number [boolean] -t, --transform path to the transform file. Can be either a local path or url. Defaults to ./astx.ts or ./astx.js if --find isn't given --parser parser to use (options: babel, babel/auto, recast/babel, recast/babel/auto) [string] --parserOptions options for parser [string] -f, --find search pattern [string] -r, --replace replace pattern [string] -y, --yes don't ask for confirmation before writing changes [boolean] --gitignore ignore gitignored files [boolean] [default: true] --workers number of worker threads to use [number]
About
Super powerful structural search and replace for JavaScript and TypeScript to automate your refactoring