v1.0 with SW PWA enabled

This commit is contained in:
Blomios
2026-01-01 17:40:53 +01:00
parent 1c0e22aac1
commit 3c8bebb2ad
29775 changed files with 2197201 additions and 119080 deletions

View File

@ -0,0 +1,208 @@
module.exports = ({ root, jscodeshift }) => {
const findImportIdentifierOf = (importSpecifiers, identifier) => {
const specifier = importSpecifiers
.filter((node) => node.value.imported.name === identifier)
.paths()
if (specifier.length > 0) {
return specifier[0].value.local
}
return jscodeshift.identifier(identifier)
}
const findImportSpecifiers = (packageName) =>
root
.find(jscodeshift.ImportDeclaration, {
source: {
value: packageName,
},
})
.find(jscodeshift.ImportSpecifier, {})
const locateImports = (
identifiers,
packageName = '@tanstack/react-query',
) => {
const findNamespaceImportIdentifier = () => {
const specifier = root
.find(jscodeshift.ImportDeclaration, {
source: {
value: packageName,
},
})
.find(jscodeshift.ImportNamespaceSpecifier)
.paths()
return specifier.length > 0 ? specifier[0].value.local : null
}
/**
* First, we search for the namespace import identifier because if we have any, we assume the consumer uses
* namespace imports. In this case, we won't search for named imports at all.
*/
const namespaceImportIdentifier = findNamespaceImportIdentifier()
if (namespaceImportIdentifier) {
const identifierMap = {}
for (const identifier of identifiers) {
identifierMap[identifier] = jscodeshift.identifier(identifier)
}
return {
namespace: namespaceImportIdentifier,
...identifierMap,
}
}
const importSpecifiers = findImportSpecifiers(packageName)
const identifierMap = {}
for (const identifier of identifiers) {
identifierMap[identifier] = findImportIdentifierOf(
importSpecifiers,
identifier,
)
}
return {
namespace: null,
...identifierMap,
}
}
const findAllMethodCalls = () =>
root
// First, we need to find all method calls.
.find(jscodeshift.CallExpression, {
callee: {
type: jscodeshift.MemberExpression.name,
property: {
type: jscodeshift.Identifier.name,
},
},
})
const findQueryClientIdentifiers = (importIdentifiers) =>
root
.find(jscodeshift.VariableDeclarator, {})
.filter((node) => {
if (node.value.init) {
const initializer = node.value.init
return (
isClassInstantiationOf(
initializer,
getSelectorByImports(importIdentifiers, 'QueryClient'),
) ||
isFunctionCallOf(
initializer,
getSelectorByImports(importIdentifiers, 'useQueryClient'),
)
)
}
return false
})
.paths()
.map((node) => node.value.id.name)
const isCallExpression = (node) =>
jscodeshift.match(node, { type: jscodeshift.CallExpression.name })
const isIdentifier = (node) =>
jscodeshift.match(node, { type: jscodeshift.Identifier.name })
const isMemberExpression = (node) =>
jscodeshift.match(node, { type: jscodeshift.MemberExpression.name })
const isNewExpression = (node) =>
jscodeshift.match(node, { type: jscodeshift.NewExpression.name })
const isArrayExpression = (node) =>
jscodeshift.match(node, { type: jscodeshift.ArrayExpression.name })
const isObjectExpression = (node) =>
jscodeshift.match(node, { type: jscodeshift.ObjectExpression.name })
const isObjectProperty = (node) =>
jscodeshift.match(node, { type: jscodeshift.ObjectProperty.name })
const isSpreadElement = (node) =>
jscodeshift.match(node, { type: jscodeshift.SpreadElement.name })
/**
* @param {import('jscodeshift').Node} node
* @returns {boolean}
*/
const isFunctionDefinition = (node) => {
const isArrowFunctionExpression = jscodeshift.match(node, {
type: jscodeshift.ArrowFunctionExpression.name,
})
const isFunctionExpression = jscodeshift.match(node, {
type: jscodeshift.FunctionExpression.name,
})
return isArrowFunctionExpression || isFunctionExpression
}
const warn = (message) => {
if (process.env.NODE_ENV !== 'test') {
console.warn(message)
}
}
const isClassInstantiationOf = (node, selector) => {
if (!isNewExpression(node)) {
return false
}
const parts = selector.split('.')
return parts.length === 1
? isIdentifier(node.callee) && node.callee.name === parts[0]
: isMemberExpression(node.callee) &&
node.callee.object.name === parts[0] &&
node.callee.property.name === parts[1]
}
const isFunctionCallOf = (node, selector) => {
if (!isCallExpression(node)) {
return false
}
const parts = selector.split('.')
return parts.length === 1
? isIdentifier(node.callee) && node.callee.name === parts[0]
: isMemberExpression(node.callee) &&
node.callee.object.name === parts[0] &&
node.callee.property.name === parts[1]
}
const getSelectorByImports = (imports, path) =>
imports.namespace
? `${imports.namespace.name}.${imports[path].name}`
: imports[path].name
return {
findAllMethodCalls,
getSelectorByImports,
isCallExpression,
isClassInstantiationOf,
isFunctionCallOf,
isIdentifier,
isMemberExpression,
isArrayExpression,
isObjectExpression,
isObjectProperty,
isSpreadElement,
isFunctionDefinition,
locateImports,
warn,
queryClient: {
findQueryClientIdentifiers,
},
}
}

View File

@ -0,0 +1,124 @@
module.exports = ({
jscodeshift,
utils,
root,
packageName = '@tanstack/react-query',
}) => {
const isGetQueryCacheMethodCall = (
initializer,
importIdentifiers,
knownQueryClientIds,
) => {
const isKnownQueryClient = (node) =>
utils.isIdentifier(node) && knownQueryClientIds.includes(node.name)
const isGetQueryCacheIdentifier = (node) =>
utils.isIdentifier(node) && node.name === 'getQueryCache'
const isValidInitializer = (node) =>
utils.isCallExpression(node) && utils.isMemberExpression(node.callee)
if (isValidInitializer(initializer)) {
const instance = initializer.callee.object
return (
isGetQueryCacheIdentifier(initializer.callee.property) &&
(isKnownQueryClient(instance) ||
utils.isFunctionCallOf(
instance,
utils.getSelectorByImports(importIdentifiers, 'useQueryClient'),
))
)
}
return false
}
const findQueryCacheInstantiations = (
importIdentifiers,
knownQueryClientIds,
) =>
root.find(jscodeshift.VariableDeclarator, {}).filter((node) => {
if (node.value.init) {
const initializer = node.value.init
return (
utils.isClassInstantiationOf(
initializer,
utils.getSelectorByImports(importIdentifiers, 'QueryCache'),
) ||
isGetQueryCacheMethodCall(
initializer,
importIdentifiers,
knownQueryClientIds,
)
)
}
return false
})
const filterQueryCacheMethodCalls = (node) =>
utils.isIdentifier(node) && ['find', 'findAll'].includes(node.name)
const findQueryCacheMethodCalls = (importIdentifiers) => {
/**
* Here we collect all query client instantiations. We have to make aware of them because the query cache can be
* accessed by the query client as well.
*/
const queryClientIdentifiers =
utils.queryClient.findQueryClientIdentifiers(importIdentifiers)
/**
* Here we collect all query cache instantiations. The reason is simple: the methods can be called on query cache
* instances, to locate the possible usages we need to be aware of the identifier names.
*/
const queryCacheIdentifiers = findQueryCacheInstantiations(
importIdentifiers,
queryClientIdentifiers,
)
.paths()
.map((node) => node.value.id.name)
return (
utils
// First, we need to find all method calls.
.findAllMethodCalls()
// Then we narrow the collection to all `fetch` and `fetchAll` methods.
.filter((node) =>
filterQueryCacheMethodCalls(node.value.callee.property),
)
.filter((node) => {
const object = node.value.callee.object
// If the method is called on a `QueryCache` instance, we keep it in the collection.
if (utils.isIdentifier(object)) {
return queryCacheIdentifiers.includes(object.name)
}
// If the method is called on a `QueryClient` instance, we keep it in the collection.
if (utils.isCallExpression(object)) {
return isGetQueryCacheMethodCall(
object,
importIdentifiers,
queryClientIdentifiers,
)
}
return false
})
)
}
const execute = (replacer) => {
findQueryCacheMethodCalls(
utils.locateImports(
['QueryCache', 'QueryClient', 'useQueryClient'],
packageName,
),
).replaceWith(replacer)
}
return {
execute,
}
}

View File

@ -0,0 +1,53 @@
module.exports = ({
jscodeshift,
utils,
root,
packageName = '@tanstack/react-query',
}) => {
const filterQueryClientMethodCalls = (node, methods) =>
utils.isIdentifier(node) && methods.includes(node.name)
const findQueryClientMethodCalls = (importIdentifiers, methods) => {
/**
* Here we collect all query client instantiations. We have to make aware of them because some method calls might
* be invoked on these instances.
*/
const queryClientIdentifiers =
utils.queryClient.findQueryClientIdentifiers(importIdentifiers)
return (
utils
// First, we need to find all method calls.
.findAllMethodCalls()
// Then we narrow the collection to `QueryClient` methods.
.filter((node) =>
filterQueryClientMethodCalls(node.value.callee.property, methods),
)
.filter((node) => {
const object = node.value.callee.object
// If the method is called on a `QueryClient` instance, we keep it in the collection.
if (utils.isIdentifier(object)) {
return queryClientIdentifiers.includes(object.name)
}
// If the method is called on the return value of `useQueryClient` hook, we keep it in the collection.
return utils.isFunctionCallOf(
object,
utils.getSelectorByImports(importIdentifiers, 'useQueryClient'),
)
})
)
}
const execute = (methods, replacer) => {
findQueryClientMethodCalls(
utils.locateImports(['QueryClient', 'useQueryClient'], packageName),
methods,
).replaceWith(replacer)
}
return {
execute,
}
}

View File

@ -0,0 +1,38 @@
module.exports = ({
jscodeshift,
utils,
root,
packageName = '@tanstack/react-query',
}) => {
const filterUseQueryLikeHookCalls = (node, importIdentifiers, hooks) => {
for (const hook of hooks) {
const selector = utils.getSelectorByImports(importIdentifiers, hook)
if (utils.isFunctionCallOf(node, selector)) {
return true
}
}
return false
}
const findUseQueryLikeHookCalls = (importIdentifiers, hooks) =>
root
// First, we need to find all call expressions.
.find(jscodeshift.CallExpression, {})
// Then we narrow the collection to the `useQuery` like hook calls.
.filter((node) =>
filterUseQueryLikeHookCalls(node.value, importIdentifiers, hooks),
)
const execute = (hooks, replacer) => {
findUseQueryLikeHookCalls(
utils.locateImports(hooks, packageName),
hooks,
).replaceWith(replacer)
}
return {
execute,
}
}

View File

@ -0,0 +1,181 @@
const createUtilsObject = require('../utils/index.cjs')
const createKeyReplacer = require('./utils/replacers/key-replacer.cjs')
const createUseQueryLikeTransformer = require('../utils/transformers/use-query-like-transformer.cjs')
const createQueryClientTransformer = require('../utils/transformers/query-client-transformer.cjs')
const createQueryCacheTransformer = require('../utils/transformers/query-cache-transformer.cjs')
const transformQueryClientUsages = ({
jscodeshift,
utils,
root,
filePath,
packageName,
}) => {
const transformer = createQueryClientTransformer({
jscodeshift,
utils,
root,
packageName,
})
const replacer = createKeyReplacer({ jscodeshift, root, filePath })
transformer.execute(
[
// Not object syntax-aware methods.
'getMutationDefaults',
'getQueriesData',
'getQueryData',
'getQueryDefaults',
'getQueryState',
'isFetching',
'setMutationDefaults',
'setQueriesData',
'setQueryData',
'setQueryDefaults',
// Object syntax-aware methods.
'cancelQueries',
'fetchInfiniteQuery',
'fetchQuery',
'invalidateQueries',
'prefetchInfiniteQuery',
'prefetchQuery',
'refetchQueries',
'removeQueries',
'resetQueries',
],
replacer,
)
}
const transformUseQueriesUsages = ({
jscodeshift,
utils,
root,
packageName,
}) => {
const transformer = createUseQueryLikeTransformer({
jscodeshift,
utils,
root,
packageName,
})
const replacer = ({ node }) => {
/**
* When the node doesn't have the 'original' property, that means the codemod has been already applied,
* so we don't need to do any changes.
*/
if (!node.original) {
return node
}
const newCallExpression = jscodeshift.callExpression(node.original.callee, [
jscodeshift.objectExpression([
jscodeshift.property(
'init',
jscodeshift.identifier('queries'),
node.original.arguments[0],
),
]),
])
// TODO: This should be part of one function!
if (node.typeParameters) {
newCallExpression.typeArguments = node.typeParameters
}
return newCallExpression
}
transformer.execute(['useQueries'], replacer)
}
const transformUseQueryLikeUsages = ({
jscodeshift,
utils,
root,
filePath,
packageName,
}) => {
const transformer = createUseQueryLikeTransformer({
jscodeshift,
utils,
root,
packageName,
})
transformer.execute(
['useQuery', 'useInfiniteQuery', 'useIsFetching', 'useIsMutating'],
createKeyReplacer({
jscodeshift,
root,
filePath,
keyName: 'queryKey',
}),
)
transformer.execute(
['useMutation'],
createKeyReplacer({
jscodeshift,
root,
filePath,
keyName: 'mutationKey',
}),
)
}
const transformQueryCacheUsages = ({
jscodeshift,
utils,
root,
filePath,
packageName,
}) => {
const transformer = createQueryCacheTransformer({
jscodeshift,
utils,
root,
packageName,
})
const replacer = createKeyReplacer({ jscodeshift, root, filePath })
transformer.execute(replacer)
}
module.exports = (file, api) => {
const jscodeshift = api.jscodeshift
const root = jscodeshift(file.source)
// TODO: Execute the transformers only when it contains a `react-query` import!
const utils = createUtilsObject({ root, jscodeshift })
const filePath = file.path
const packageName = 'react-query'
// This function transforms usages like `useQuery` and `useMutation`.
transformUseQueryLikeUsages({
jscodeshift,
utils,
root,
filePath,
packageName,
})
// This function transforms usages of `useQueries`.
transformUseQueriesUsages({
jscodeshift,
utils,
root,
packageName,
})
// This function transforms usages of `QueryClient`.
transformQueryClientUsages({
jscodeshift,
utils,
root,
filePath,
packageName,
})
// This function transforms usages of `QueryCache`.
transformQueryCacheUsages({ jscodeshift, utils, root, filePath, packageName })
return root.toSource({ quote: 'single', lineTerminator: '\n' })
}

View File

@ -0,0 +1,25 @@
module.exports = (file, api) => {
const jscodeshift = api.jscodeshift
const root = jscodeshift(file.source)
const replacements = [
{ from: 'react-query', to: '@tanstack/react-query' },
{ from: 'react-query/devtools', to: '@tanstack/react-query-devtools' },
]
replacements.forEach(({ from, to }) => {
root
.find(jscodeshift.ImportDeclaration, {
source: {
value: from,
},
})
.replaceWith(({ node }) => {
node.source.value = to
return node
})
})
return root.toSource({ quote: 'single', lineTerminator: '\n' })
}

View File

@ -0,0 +1,164 @@
class UnprocessableKeyError extends Error {
constructor(message) {
super(message)
this.name = 'UnprocessableKeyError'
}
}
module.exports = ({ jscodeshift, root, filePath, keyName = 'queryKey' }) => {
const isArrayExpression = (node) =>
jscodeshift.match(node, { type: jscodeshift.ArrayExpression.name })
const isStringLiteral = (node) =>
jscodeshift.match(node, { type: jscodeshift.StringLiteral.name }) ||
jscodeshift.match(node, { type: jscodeshift.Literal.name })
const isTemplateLiteral = (node) =>
jscodeshift.match(node, { type: jscodeshift.TemplateLiteral.name })
const findVariableDeclaration = (node) => {
const declarations = root
.find(jscodeshift.VariableDeclarator, {
id: {
type: jscodeshift.Identifier.name,
name: node.name,
},
})
.paths()
return declarations.length > 0 ? declarations[0] : null
}
const createKeyValue = (node) => {
// When the node is a string literal we convert it into an array of strings.
if (isStringLiteral(node)) {
return jscodeshift.arrayExpression([
jscodeshift.stringLiteral(node.value),
])
}
// When the node is a template literal we convert it into an array of template literals.
if (isTemplateLiteral(node)) {
return jscodeshift.arrayExpression([
jscodeshift.templateLiteral(node.quasis, node.expressions),
])
}
if (jscodeshift.match(node, { type: jscodeshift.Identifier.name })) {
// When the node is an identifier at first, we try to find its declaration, because we will try
// to guess its type.
const variableDeclaration = findVariableDeclaration(node)
if (!variableDeclaration) {
throw new UnprocessableKeyError(
`In file ${filePath} at line ${node.loc.start.line} the type of identifier \`${node.name}\` couldn't be recognized, so the codemod couldn't be applied. Please migrate manually.`,
)
}
const initializer = variableDeclaration.value.init
// When it's a string, we just wrap it into an array expression.
if (isStringLiteral(initializer) || isTemplateLiteral(initializer)) {
return jscodeshift.arrayExpression([node])
}
}
throw new UnprocessableKeyError(
`In file ${filePath} at line ${node.loc.start.line} the type of the \`${keyName}\` couldn't be recognized, so the codemod couldn't be applied. Please migrate manually.`,
)
}
const createKeyProperty = (node) =>
jscodeshift.property(
'init',
jscodeshift.identifier(keyName),
createKeyValue(node),
)
const getPropertyFromObjectExpression = (objectExpression, propertyName) =>
objectExpression.properties.find(
(property) => property.key.name === propertyName,
) ?? null
const buildWithTypeArguments = (node, builder) => {
const newNode = builder(node)
if (node.typeParameters) {
newNode.typeArguments = node.typeParameters
}
return newNode
}
return ({ node }) => {
// When the node doesn't have the 'original' property, that means the codemod has been already applied,
// so we don't need to do any changes.
if (!node.original) {
return node
}
const methodArguments = node.arguments
// The method call doesn't have any arguments, we have nothing to do in this case.
if (methodArguments.length === 0) {
return node
}
try {
const [firstArgument, ...restOfTheArguments] = methodArguments
if (
jscodeshift.match(firstArgument, {
type: jscodeshift.ObjectExpression.name,
})
) {
const originalKey = getPropertyFromObjectExpression(
firstArgument,
keyName,
)
if (!originalKey) {
throw new UnprocessableKeyError(
`In file ${filePath} at line ${node.loc.start.line} the \`${keyName}\` couldn't be found. Did you forget to add it?`,
)
}
const restOfTheProperties = firstArgument.properties.filter(
(item) => item.key.name !== keyName,
)
return buildWithTypeArguments(node, (originalNode) =>
jscodeshift.callExpression(originalNode.original.callee, [
jscodeshift.objectExpression([
createKeyProperty(originalKey.value),
...restOfTheProperties,
]),
...restOfTheArguments,
]),
)
}
// When the node is an array expression we just simply return it because we want query keys to be arrays.
if (isArrayExpression(firstArgument)) {
return node
}
return buildWithTypeArguments(node, (originalNode) =>
jscodeshift.callExpression(originalNode.original.callee, [
createKeyValue(firstArgument),
...restOfTheArguments,
]),
)
} catch (error) {
if (error.name === 'UnprocessableKeyError') {
if (process.env.NODE_ENV !== 'test') {
console.warn(error.message)
}
return node
}
throw error
}
}
}

View File

@ -0,0 +1,244 @@
const createUtilsObject = require('../../utils/index.cjs')
const createUseQueryLikeTransformer = require('../../utils/transformers/use-query-like-transformer.cjs')
const createQueryClientTransformer = require('../../utils/transformers/query-client-transformer.cjs')
const originalName = 'isLoading'
const newName = 'isPending'
/**
* @param {import('jscodeshift')} jscodeshift
* @param {Object} utils
* @param {import('jscodeshift').Collection} root
* @param {string} filePath
* @param {{keyName: "mutationKey"|"queryKey", queryClientMethods: ReadonlyArray<string>, hooks: ReadonlyArray<string>}} config
*/
const transformUsages = ({ jscodeshift, utils, root, filePath, config }) => {
/**
* @param {import('jscodeshift').CallExpression | import('jscodeshift').ExpressionStatement} node
* @returns {{start: number, end: number}}
*/
const getNodeLocation = (node) => {
const location = utils.isCallExpression(node) ? node.callee.loc : node.loc
const start = location.start.line
const end = location.end.line
return { start, end }
}
/**
* @param {import('jscodeshift').ASTNode} node
* @returns {boolean}
*/
const isObjectExpression = (node) => {
return jscodeshift.match(node, {
type: jscodeshift.ObjectExpression.name,
})
}
/**
* @param {import('jscodeshift').ASTNode} node
* @returns {boolean}
*/
const isObjectPattern = (node) => {
return jscodeshift.match(node, {
type: jscodeshift.ObjectPattern.name,
})
}
/**
* @param {import('jscodeshift').ASTNode} node
* @returns {boolean}
*/
const isVariableDeclarator = (node) => {
return jscodeshift.match(node, {
type: jscodeshift.VariableDeclarator.name,
})
}
/**
* @param {import('jscodeshift').Node} node
* @param {import('jscodeshift').Identifier} identifier
* @returns {Collection<import('jscodeshift').MemberExpression>}
*/
const findIsLoadingPropertiesOfIdentifier = (node, identifier) => {
return jscodeshift(node).find(jscodeshift.MemberExpression, {
object: {
type: jscodeshift.Identifier.name,
name: identifier.name,
},
property: {
type: jscodeshift.Identifier.name,
name: originalName,
},
})
}
/**
* @param {import('jscodeshift').ObjectPattern} node
* @returns {import('jscodeshift').ObjectProperty|null}
*/
const findIsLoadingObjectPropertyInObjectPattern = (node) => {
return (
node.properties.find((property) =>
jscodeshift.match(property, {
key: {
type: jscodeshift.Identifier.name,
name: originalName,
},
}),
) ?? null
)
}
/**
* @param {import('jscodeshift').ObjectPattern} node
* @returns {import('jscodeshift').RestElement|null}
*/
const findRestElementInObjectPattern = (node) => {
return (
node.properties.find((property) =>
jscodeshift.match(property, {
type: jscodeshift.RestElement.name,
}),
) ?? null
)
}
const replacer = (path, transformNode) => {
const node = path.node
const parentNode = path.parentPath.value
const { start, end } = getNodeLocation(node)
try {
if (!isVariableDeclarator(parentNode)) {
// The parent node is not a variable declarator, the transformation will be skipped.
return node
}
const lookupNode = path.scope.node
const variableDeclaratorId = parentNode.id
if (isObjectPattern(variableDeclaratorId)) {
const isLoadingObjectProperty =
findIsLoadingObjectPropertyInObjectPattern(variableDeclaratorId)
if (isLoadingObjectProperty) {
jscodeshift(lookupNode)
.find(jscodeshift.ObjectProperty, {
key: {
type: jscodeshift.Identifier.name,
name: originalName,
},
})
.replaceWith((mutablePath) => {
if (isObjectPattern(mutablePath.parent)) {
const affectedProperty = mutablePath.value.value.shorthand
? 'value'
: 'key'
mutablePath.value[affectedProperty].name = newName
return mutablePath.value
}
if (isObjectExpression(mutablePath.parent)) {
const affectedProperty = mutablePath.value.value.shorthand
? 'key'
: 'value'
mutablePath.value[affectedProperty].name = newName
return mutablePath.value
}
return mutablePath.value
})
// Renaming all other 'isLoading' references that are object properties.
jscodeshift(lookupNode)
.find(jscodeshift.Identifier, { name: originalName })
.replaceWith((mutablePath) => {
if (
!jscodeshift.match(mutablePath.parent, {
type: jscodeshift.ObjectProperty.name,
})
) {
mutablePath.value.name = newName
}
return mutablePath.value
})
}
const restElement = findRestElementInObjectPattern(variableDeclaratorId)
if (restElement) {
findIsLoadingPropertiesOfIdentifier(
lookupNode,
restElement.argument,
).replaceWith(({ node: mutableNode }) => {
mutableNode.property.name = newName
return mutableNode
})
}
return node
}
if (utils.isIdentifier(variableDeclaratorId)) {
findIsLoadingPropertiesOfIdentifier(
lookupNode,
variableDeclaratorId,
).replaceWith(({ node: mutableNode }) => {
mutableNode.property.name = newName
return mutableNode
})
return node
}
utils.warn(
`The usage in file "${filePath}" at line ${start}:${end} could not be transformed. Please migrate this usage manually.`,
)
return node
} catch (error) {
utils.warn(
`An unknown error occurred while processing the "${filePath}" file. Please review this file, because the codemod couldn't be applied.`,
)
return node
}
}
createUseQueryLikeTransformer({ jscodeshift, utils, root }).execute(
config.hooks,
replacer,
)
createQueryClientTransformer({ jscodeshift, utils, root }).execute(
config.queryClientMethods,
replacer,
)
}
module.exports = (file, api) => {
const jscodeshift = api.jscodeshift
const root = jscodeshift(file.source)
const utils = createUtilsObject({ root, jscodeshift })
const filePath = file.path
const dependencies = { jscodeshift, utils, root, filePath }
transformUsages({
...dependencies,
config: {
hooks: ['useQuery', 'useMutation'],
queryClientMethods: [],
},
})
return root.toSource({ quote: 'single', lineTerminator: '\n' })
}

View File

@ -0,0 +1,32 @@
### Intro
The prerequisite for this code mod is to migrate your usages to the new syntax, so overloads for hooks and `QueryClient` methods shouldn't be available anymore.
### Affected usages
Please note, this code mod transforms usages only where the first argument is an object expression.
The following usage should be transformed by the code mod:
```ts
const { data } = useQuery({
queryKey: ['posts'],
queryFn: queryFn,
keepPreviousData: true,
})
```
But the following usage won't be transformed by the code mod, because the first argument an identifier:
```ts
const hookArgument = {
queryKey: ['posts'],
queryFn: queryFn,
keepPreviousData: true,
}
const { data } = useQuery(hookArgument)
```
### Troubleshooting
In case of any errors, feel free to reach us out via Discord or open an issue. If you open an issue, please provide a code snippet as well, because without a snippet we cannot find the bug in the code mod.

View File

@ -0,0 +1,271 @@
const createUtilsObject = require('../../utils/index.cjs')
const createUseQueryLikeTransformer = require('../../utils/transformers/use-query-like-transformer.cjs')
const createQueryClientTransformer = require('../../utils/transformers/query-client-transformer.cjs')
const AlreadyHasPlaceholderDataProperty = require('./utils/already-has-placeholder-data-property.cjs')
/**
* @param {import('jscodeshift')} jscodeshift
* @param {Object} utils
* @param {import('jscodeshift').Collection} root
* @param {string} filePath
* @param {{keyName: "mutationKey"|"queryKey", queryClientMethods: ReadonlyArray<string>, hooks: ReadonlyArray<string>}} config
*/
const transformUsages = ({ jscodeshift, utils, root, filePath, config }) => {
/**
* @param {import('jscodeshift').CallExpression | import('jscodeshift').ExpressionStatement} node
* @returns {{start: number, end: number}}
*/
const getNodeLocation = (node) => {
const location = utils.isCallExpression(node) ? node.callee.loc : node.loc
const start = location.start.line
const end = location.end.line
return { start, end }
}
/**
* @param {import('jscodeshift').ObjectProperty} objectProperty
* @returns {boolean}
*/
const isKeepPreviousDataObjectProperty = (objectProperty) => {
return jscodeshift.match(objectProperty.key, {
type: jscodeshift.Identifier.name,
name: 'keepPreviousData',
})
}
/**
* @param {import('jscodeshift').ObjectProperty} objectProperty
* @returns {boolean}
*/
const isObjectPropertyHasTrueBooleanLiteralValue = (objectProperty) => {
return jscodeshift.match(objectProperty.value, {
type: jscodeshift.BooleanLiteral.name,
value: true,
})
}
/**
* @param {import('jscodeshift').ObjectExpression} objectExpression
* @returns {Array<import('jscodeshift').ObjectProperty>}
*/
const filterKeepPreviousDataProperty = (objectExpression) => {
return objectExpression.properties.filter((objectProperty) => {
return !isKeepPreviousDataObjectProperty(objectProperty)
})
}
const createPlaceholderDataObjectProperty = () => {
return jscodeshift.objectProperty(
jscodeshift.identifier('placeholderData'),
jscodeshift.identifier('keepPreviousData'),
)
}
/**
* @param {import('jscodeshift').ObjectExpression} objectExpression
* @returns {boolean}
*/
const hasPlaceholderDataProperty = (objectExpression) => {
return (
objectExpression.properties.findIndex((objectProperty) => {
return jscodeshift.match(objectProperty.key, {
type: jscodeshift.Identifier.name,
name: 'placeholderData',
})
}) !== -1
)
}
/**
* @param {import('jscodeshift').ObjectExpression} objectExpression
* @returns {import('jscodeshift').ObjectProperty | undefined}
*/
const getKeepPreviousDataProperty = (objectExpression) => {
return objectExpression.properties.find(isKeepPreviousDataObjectProperty)
}
let shouldAddKeepPreviousDataImport = false
const replacer = (path, resolveTargetArgument, transformNode) => {
const node = path.node
const { start, end } = getNodeLocation(node)
try {
const targetArgument = resolveTargetArgument(node)
if (targetArgument && utils.isObjectExpression(targetArgument)) {
const isPlaceholderDataPropertyPresent =
hasPlaceholderDataProperty(targetArgument)
if (hasPlaceholderDataProperty(targetArgument)) {
throw new AlreadyHasPlaceholderDataProperty(node, filePath)
}
const keepPreviousDataProperty =
getKeepPreviousDataProperty(targetArgument)
const keepPreviousDataPropertyHasTrueValue =
isObjectPropertyHasTrueBooleanLiteralValue(keepPreviousDataProperty)
if (!keepPreviousDataPropertyHasTrueValue) {
utils.warn(
`The usage in file "${filePath}" at line ${start}:${end} already contains a "keepPreviousData" property but its value is not "true". Please migrate this usage manually.`,
)
return node
}
if (keepPreviousDataPropertyHasTrueValue) {
// Removing the `keepPreviousData` property from the object.
const mutableObjectExpressionProperties =
filterKeepPreviousDataProperty(targetArgument)
if (!isPlaceholderDataPropertyPresent) {
shouldAddKeepPreviousDataImport = true
// When the `placeholderData` property is not present, the `placeholderData: keepPreviousData` property will be added.
mutableObjectExpressionProperties.push(
createPlaceholderDataObjectProperty(),
)
}
return transformNode(
node,
jscodeshift.objectExpression(mutableObjectExpressionProperties),
)
}
}
utils.warn(
`The usage in file "${filePath}" at line ${start}:${end} could not be transformed, because the first parameter is not an object expression. Please migrate this usage manually.`,
)
return node
} catch (error) {
utils.warn(
error.name === AlreadyHasPlaceholderDataProperty.name
? error.message
: `An unknown error occurred while processing the "${filePath}" file. Please review this file, because the codemod couldn't be applied.`,
)
return node
}
}
createUseQueryLikeTransformer({ jscodeshift, utils, root }).execute(
config.hooks,
(path) => {
const resolveTargetArgument = (node) => node.arguments[0] ?? null
const transformNode = (node, transformedArgument) =>
jscodeshift.callExpression(node.original.callee, [transformedArgument])
return replacer(path, resolveTargetArgument, transformNode)
},
)
createQueryClientTransformer({ jscodeshift, utils, root }).execute(
config.queryClientMethods,
(path) => {
const resolveTargetArgument = (node) => node.arguments[1] ?? null
const transformNode = (node, transformedArgument) => {
return jscodeshift.callExpression(node.original.callee, [
node.arguments[0],
transformedArgument,
...node.arguments.slice(2, 0),
])
}
return replacer(path, resolveTargetArgument, transformNode)
},
)
const importIdentifierOfQueryClient = utils.getSelectorByImports(
utils.locateImports(['QueryClient']),
'QueryClient',
)
root
.find(jscodeshift.ExpressionStatement, {
expression: {
type: jscodeshift.NewExpression.name,
callee: {
type: jscodeshift.Identifier.name,
name: importIdentifierOfQueryClient,
},
},
})
.filter((path) => path.node.expression)
.replaceWith((path) => {
const resolveTargetArgument = (node) => {
const paths = jscodeshift(node)
.find(jscodeshift.ObjectProperty, {
key: {
type: jscodeshift.Identifier.name,
name: 'keepPreviousData',
},
})
.paths()
return paths.length > 0 ? paths[0].parent.node : null
}
const transformNode = (node, transformedArgument) => {
jscodeshift(node.expression)
.find(jscodeshift.ObjectProperty, {
key: {
type: jscodeshift.Identifier.name,
name: 'queries',
},
})
.replaceWith(({ node: mutableNode }) => {
mutableNode.value.properties = transformedArgument.properties
return mutableNode
})
return node
}
return replacer(path, resolveTargetArgument, transformNode)
})
return { shouldAddKeepPreviousDataImport }
}
module.exports = (file, api) => {
const jscodeshift = api.jscodeshift
const root = jscodeshift(file.source)
const utils = createUtilsObject({ root, jscodeshift })
const filePath = file.path
const dependencies = { jscodeshift, utils, root, filePath }
const { shouldAddKeepPreviousDataImport } = transformUsages({
...dependencies,
config: {
hooks: ['useInfiniteQuery', 'useQueries', 'useQuery'],
queryClientMethods: ['setQueryDefaults'],
},
})
if (shouldAddKeepPreviousDataImport) {
root
.find(jscodeshift.ImportDeclaration, {
source: {
value: '@tanstack/react-query',
},
})
.replaceWith(({ node: mutableNode }) => {
mutableNode.specifiers = [
jscodeshift.importSpecifier(
jscodeshift.identifier('keepPreviousData'),
),
...mutableNode.specifiers,
]
return mutableNode
})
}
return root.toSource({ quote: 'single', lineTerminator: '\n' })
}

View File

@ -0,0 +1,26 @@
class AlreadyHasPlaceholderDataProperty extends Error {
/**
* @param {import('jscodeshift').CallExpression} callExpression
* @param {string} filePath
*/
constructor(callExpression, filePath) {
super('')
this.message = this.buildMessage(callExpression, filePath)
this.name = 'AlreadyHasPlaceholderDataProperty'
}
/**
* @param {import('jscodeshift').CallExpression} callExpression
* @param {string} filePath
* @returns {string}
*/
buildMessage(callExpression, filePath) {
const location = callExpression.callee.loc
const start = location.start.line
const end = location.end.line
return `The usage in file "${filePath}" at line ${start}:${end} already contains a a "placeholderData" property. Please migrate this usage manually.`
}
}
module.exports = AlreadyHasPlaceholderDataProperty

View File

@ -0,0 +1,58 @@
const createUtilsObject = require('../../utils/index.cjs')
const transformFilterAwareUsages = require('./transformers/filter-aware-usage-transformer.cjs')
const transformQueryFnAwareUsages = require('./transformers/query-fn-aware-usage-transformer.cjs')
module.exports = (file, api) => {
const jscodeshift = api.jscodeshift
const root = jscodeshift(file.source)
const utils = createUtilsObject({ root, jscodeshift })
const filePath = file.path
const dependencies = { jscodeshift, utils, root, filePath }
transformFilterAwareUsages({
...dependencies,
config: {
keyName: 'queryKey',
fnName: 'queryFn',
queryClientMethods: [
'cancelQueries',
'getQueriesData',
'invalidateQueries',
'isFetching',
'refetchQueries',
'removeQueries',
'resetQueries',
// 'setQueriesData',
],
hooks: ['useIsFetching', 'useQuery'],
},
})
transformFilterAwareUsages({
...dependencies,
config: {
keyName: 'mutationKey',
fnName: 'mutationFn',
queryClientMethods: [],
hooks: ['useIsMutating', 'useMutation'],
},
})
transformQueryFnAwareUsages({
...dependencies,
config: {
keyName: 'queryKey',
queryClientMethods: [
'ensureQueryData',
'fetchQuery',
'prefetchQuery',
'fetchInfiniteQuery',
'prefetchInfiniteQuery',
],
hooks: [],
},
})
return root.toSource({ quote: 'single', lineTerminator: '\n' })
}

View File

@ -0,0 +1,271 @@
const createV5UtilsObject = require('../utils/index.cjs')
const UnknownUsageError = require('../utils/unknown-usage-error.cjs')
const createQueryClientTransformer = require('../../../utils/transformers/query-client-transformer.cjs')
const createQueryCacheTransformer = require('../../../utils/transformers/query-cache-transformer.cjs')
const createUseQueryLikeTransformer = require('../../../utils/transformers/use-query-like-transformer.cjs')
/**
* @param {import('jscodeshift').api} jscodeshift
* @param {Object} utils
* @param {import('jscodeshift').Collection} root
* @param {string} filePath
* @param {{keyName: "mutationKey"|"queryKey", fnName: "mutationFn"|"queryFn", queryClientMethods: ReadonlyArray<string>, hooks: ReadonlyArray<string>}} config
*/
const transformFilterAwareUsages = ({
jscodeshift,
utils,
root,
filePath,
config,
}) => {
const v5Utils = createV5UtilsObject({ jscodeshift, utils })
/**
* @param {import('jscodeshift').CallExpression} node
* @param {"mutationKey"|"queryKey"} keyName
* @param {"mutationFn"|"queryFn"} fnName
* @returns {boolean}
*/
const canSkipReplacement = (node, keyName, fnName) => {
const callArguments = node.arguments
const hasKeyOrFnProperty = () =>
callArguments[0].properties.some(
(property) =>
utils.isObjectProperty(property) &&
property.key.name !== keyName &&
property.key.name !== fnName,
)
/**
* This call has at least one argument. If it's an object expression and contains the "queryKey" or "mutationKey"
* field, the transformation can be skipped, because it's already matching the expected signature.
*/
return (
callArguments.length > 0 &&
utils.isObjectExpression(callArguments[0]) &&
hasKeyOrFnProperty()
)
}
/**
* This function checks whether the given object property is a spread element or a property that's not named
* "queryKey" or "mutationKey".
*
* @param {import('jscodeshift').ObjectProperty} property
* @returns {boolean}
*/
const predicate = (property) => {
const isSpreadElement = utils.isSpreadElement(property)
const isObjectProperty = utils.isObjectProperty(property)
return (
isSpreadElement ||
(isObjectProperty && property.key.name !== config.keyName)
)
}
const replacer = (path) => {
const node = path.node
const isFunctionDefinition = (functionArgument) => {
if (utils.isFunctionDefinition(functionArgument)) {
return true
}
if (utils.isIdentifier(functionArgument)) {
const binding = v5Utils.getBindingFromScope(
path,
functionArgument.name,
filePath,
)
const isVariableDeclarator = jscodeshift.match(binding, {
type: jscodeshift.VariableDeclarator.name,
})
return isVariableDeclarator && utils.isFunctionDefinition(binding.init)
}
}
try {
// If the given method/function call matches certain criteria, the node doesn't need to be replaced, this step can be skipped.
if (canSkipReplacement(node, config.keyName, config.fnName)) {
return node
}
/**
* Here we attempt to determine the first parameter of the function call.
* If it's a function definition, we can create an object property from it (the mutation fn).
*/
const firstArgument = node.arguments[0]
if (isFunctionDefinition(firstArgument)) {
const objectExpression = jscodeshift.objectExpression([
jscodeshift.property(
'init',
jscodeshift.identifier(config.fnName),
firstArgument,
),
])
const secondArgument = node.arguments[1]
if (secondArgument) {
// If it's an object expression, we can copy the properties from it to the newly created object expression.
if (utils.isObjectExpression(secondArgument)) {
v5Utils.copyPropertiesFromSource(
secondArgument,
objectExpression,
predicate,
)
} else {
// Otherwise, we simply spread the second argument in the newly created object expression.
objectExpression.properties.push(
jscodeshift.spreadElement(secondArgument),
)
}
}
return jscodeshift.callExpression(node.original.callee, [
objectExpression,
])
}
/**
* If, instead, the first parameter is an array expression or an identifier that references
* an array expression, then we create an object property from it (the query or mutation key).
*
* @type {import('jscodeshift').Property|undefined}
*/
const keyProperty = v5Utils.transformArgumentToKey(
path,
node.arguments[0],
config.keyName,
filePath,
)
/**
* The first parameter couldn't be transformed into an object property, so it's time to throw an exception,
* it will notify the consumers that they need to rewrite this usage manually.
*/
if (!keyProperty) {
const secondArgument =
node.arguments.length > 1 ? node.arguments[1] : null
if (!secondArgument) {
throw new UnknownUsageError(node, filePath)
}
if (utils.isFunctionDefinition(secondArgument)) {
const originalArguments = node.arguments
const firstArgument = jscodeshift.objectExpression([
jscodeshift.property(
'init',
jscodeshift.identifier(config.keyName),
originalArguments[0],
),
jscodeshift.property(
'init',
jscodeshift.identifier(config.fnName),
secondArgument,
),
])
return jscodeshift.callExpression(node.original.callee, [
firstArgument,
...originalArguments.slice(2),
])
}
}
const functionArguments = [jscodeshift.objectExpression([keyProperty])]
const secondParameter = node.arguments[1]
if (secondParameter) {
const createdObjectExpression = functionArguments[0]
if (isFunctionDefinition(secondParameter)) {
const objectExpression = jscodeshift.objectExpression([
jscodeshift.property(
'init',
jscodeshift.identifier(config.keyName),
node.arguments[0],
),
jscodeshift.property(
'init',
jscodeshift.identifier(config.fnName),
secondParameter,
),
])
const thirdArgument = node.arguments[2]
if (thirdArgument) {
// If it's an object expression, we can copy the properties from it to the newly created object expression.
if (utils.isObjectExpression(thirdArgument)) {
v5Utils.copyPropertiesFromSource(
thirdArgument,
objectExpression,
predicate,
)
} else {
// Otherwise, we simply spread the third argument in the newly created object expression.
objectExpression.properties.push(
jscodeshift.spreadElement(thirdArgument),
)
}
}
return jscodeshift.callExpression(node.original.callee, [
objectExpression,
])
}
/**
* If it has a second argument, and it's an object expression, then we get the properties from it
* (except the "queryKey" or "mutationKey" properties), because these arguments will also be moved to the
* newly created object expression.
*/
if (utils.isObjectExpression(secondParameter)) {
v5Utils.copyPropertiesFromSource(
secondParameter,
createdObjectExpression,
predicate,
)
} else {
// Otherwise, we simply spread the second parameter in the newly created object expression.
createdObjectExpression.properties.push(
jscodeshift.spreadElement(secondParameter),
)
}
}
// The rest of the function arguments can be simply pushed to the function arguments object so all will be kept.
functionArguments.push(...node.arguments.slice(2))
return jscodeshift.callExpression(node.original.callee, functionArguments)
} catch (error) {
utils.warn(
error.name === UnknownUsageError.name
? error.message
: `An unknown error occurred while processing the "${filePath}" file. Please review this file, because the codemod couldn't be applied.`,
)
return node
}
}
createQueryClientTransformer({ jscodeshift, utils, root }).execute(
config.queryClientMethods,
replacer,
)
createUseQueryLikeTransformer({ jscodeshift, utils, root }).execute(
config.hooks,
replacer,
)
createQueryCacheTransformer({ jscodeshift, utils, root }).execute(replacer)
}
module.exports = transformFilterAwareUsages

View File

@ -0,0 +1,185 @@
const createV5UtilsObject = require('../utils/index.cjs')
const UnknownUsageError = require('../utils/unknown-usage-error.cjs')
const createQueryClientTransformer = require('../../../utils/transformers/query-client-transformer.cjs')
/**
* @param {import('jscodeshift').api} jscodeshift
* @param {Object} utils
* @param {import('jscodeshift').Collection} root
* @param {string} filePath
* @param {{keyName: "mutationKey"|"queryKey", queryClientMethods: ReadonlyArray<string>, hooks: ReadonlyArray<string>}} config
*/
const transformQueryFnAwareUsages = ({
jscodeshift,
utils,
root,
filePath,
config,
}) => {
const v5Utils = createV5UtilsObject({ jscodeshift, utils })
/**
* @param {import('jscodeshift').CallExpression} node
* @returns {boolean}
*/
const canSkipReplacement = (node) => {
const callArguments = node.arguments
const hasKeyProperty = () =>
callArguments[0].properties.some(
(property) =>
utils.isObjectProperty(property) &&
[config.keyName, 'queryFn'].includes(property.key.name),
)
return (
callArguments.length > 0 &&
utils.isObjectExpression(callArguments[0]) &&
hasKeyProperty()
)
}
const predicate = (property) => {
const isSpreadElement = utils.isSpreadElement(property)
const isObjectProperty = utils.isObjectProperty(property)
return (
isSpreadElement ||
(isObjectProperty && property.key.name !== config.keyName)
)
}
const transformArgumentToQueryFunction = (path, node) => {
const isIdentifier = utils.isIdentifier(node)
const isFunctionDefinition = utils.isFunctionDefinition(node)
if (!isIdentifier && !isFunctionDefinition) {
return undefined
}
if (isFunctionDefinition) {
return jscodeshift.property(
'init',
jscodeshift.identifier('queryFn'),
node,
)
}
const binding = v5Utils.getBindingFromScope(path, node.name, filePath)
const initializer = v5Utils.getInitializerByDeclarator(binding)
if (!utils.isFunctionDefinition(initializer)) {
return undefined
}
return jscodeshift.property(
'init',
jscodeshift.identifier('queryFn'),
binding.id,
)
}
const transformArgumentToOptionsObject = (path, node) => {
if (!utils.isIdentifier(node)) {
return undefined
}
const binding = v5Utils.getBindingFromScope(path, node.name, filePath)
const initializer = v5Utils.getInitializerByDeclarator(binding)
if (utils.isObjectExpression(initializer)) {
return jscodeshift.spreadElement(binding.id)
}
}
const replacer = (path) => {
const node = path.node
try {
// If the given method/function call matches certain criteria, the node doesn't need to be replaced, this step can be skipped.
if (canSkipReplacement(node)) {
return node
}
const keyProperty = v5Utils.transformArgumentToKey(
path,
node.arguments[0],
config.keyName,
filePath,
)
if (!keyProperty) {
throw new UnknownUsageError(node, filePath)
}
const parameters = [jscodeshift.objectExpression([keyProperty])]
const createdObjectExpression = parameters[0]
const secondParameter = node.arguments[1]
if (secondParameter) {
const queryFnProperty = transformArgumentToQueryFunction(
path,
secondParameter,
)
if (queryFnProperty) {
createdObjectExpression.properties.push(queryFnProperty)
const thirdParameter = node.arguments[2]
if (utils.isObjectExpression(thirdParameter)) {
v5Utils.copyPropertiesFromSource(
thirdParameter,
createdObjectExpression,
predicate,
)
} else {
createdObjectExpression.properties.push(
jscodeshift.spreadElement(thirdParameter),
)
}
return jscodeshift.callExpression(node.original.callee, parameters)
}
const optionsProperty = transformArgumentToOptionsObject(
path,
secondParameter,
)
if (optionsProperty) {
createdObjectExpression.properties.push(optionsProperty)
return jscodeshift.callExpression(node.original.callee, parameters)
}
if (utils.isObjectExpression(secondParameter)) {
v5Utils.copyPropertiesFromSource(
secondParameter,
createdObjectExpression,
predicate,
)
}
return jscodeshift.callExpression(node.original.callee, parameters)
}
return jscodeshift.callExpression(node.original.callee, parameters)
} catch (error) {
utils.warn(
error.name === UnknownUsageError.name
? error.message
: `An unknown error occurred while processing the "${filePath}" file. Please review this file, because the codemod couldn't be applied.`,
)
return node
}
}
createQueryClientTransformer({ jscodeshift, utils, root }).execute(
config.queryClientMethods,
replacer,
)
}
module.exports = transformQueryFnAwareUsages

View File

@ -0,0 +1,123 @@
const UnknownUsageError = require('./unknown-usage-error.cjs')
module.exports = ({ jscodeshift, utils }) => {
/**
*
* @param {import('jscodeshift').ObjectExpression} source
* @param {import('jscodeshift').ObjectExpression} target
* @param {(node: import('jscodeshift').Node) => boolean} predicate
*/
const copyPropertiesFromSource = (source, target, predicate) => {
source.properties.forEach((property) => {
if (predicate(property)) {
target.properties.push(property)
}
})
}
/**
* @param {import('jscodeshift').NodePath} path
* @param {string} argumentName
* @param {string} filePath
* @returns {*}
*/
const getBindingFromScope = (path, argumentName, filePath) => {
/**
* If the current scope contains the declaration then we can use the actual one else we attempt to find the
* binding from above.
*/
const scope = path.scope.declares(argumentName)
? path.scope
: path.scope.lookup(argumentName)
/**
* The declaration couldn't be found for some reason, time to move on. We warn the user it needs to be rewritten
* by themselves.
*/
if (!scope) {
return undefined
}
const binding = scope.bindings[argumentName]
.filter((item) => utils.isIdentifier(item.value))
.map((item) => item.parentPath.value)
.at(0)
if (!binding) {
throw new UnknownUsageError(path.node, filePath)
}
return binding
}
/**
* @param {import('jscodeshift').VariableDeclarator} binding
* @returns {import('jscodeshift').Node|undefined}
*/
const getInitializerByDeclarator = (binding) => {
const isVariableDeclaration = jscodeshift.match(binding, {
type: jscodeshift.VariableDeclarator.name,
})
if (!isVariableDeclaration) {
return undefined
}
const isTSAsExpression = jscodeshift.match(binding.init, {
type: jscodeshift.TSAsExpression.name,
})
return isTSAsExpression ? binding.init.expression : binding.init
}
/**
* @param {import('jscodeshift').Node} node
* @returns {boolean}
*/
const isArrayExpressionVariable = (node) =>
jscodeshift.match(node, {
type: jscodeshift.VariableDeclarator.name,
init: {
type: jscodeshift.ArrayExpression.name,
},
})
/**
* @param {import('jscodeshift').NodePath} path
* @param {import('jscodeshift').Node} node
* @param {"queryKey"|"mutationKey"} keyName
* @param {string} filePath
* @returns {import('jscodeshift').Property|undefined}
*/
const transformArgumentToKey = (path, node, keyName, filePath) => {
// If the first argument is an identifier we have to infer its type if possible.
if (utils.isIdentifier(node)) {
const binding = getBindingFromScope(path, node.name, filePath)
if (isArrayExpressionVariable(binding)) {
return jscodeshift.property(
'init',
jscodeshift.identifier(keyName),
jscodeshift.identifier(binding.id.name),
)
}
}
// If the first argument is an array, then it matches the following overload:
// methodName(queryKey?: QueryKey, firstObject?: TFirstObject, secondObject?: TSecondObject)
if (utils.isArrayExpression(node)) {
// Then we create the 'queryKey' property based on it, because it will be passed to the first argument
// that should be an object according to the new signature.
return jscodeshift.property('init', jscodeshift.identifier(keyName), node)
}
return undefined
}
return {
copyPropertiesFromSource,
getInitializerByDeclarator,
getBindingFromScope,
transformArgumentToKey,
}
}

View File

@ -0,0 +1,27 @@
class UnknownUsageError extends Error {
/**
* @param {import('jscodeshift').CallExpression} callExpression
* @param {string} filePath
*/
constructor(callExpression, filePath) {
super('')
this.message = this.buildMessage(callExpression, filePath)
this.name = 'UnknownUsageError'
}
/**
*
* @param {import('jscodeshift').CallExpression} callExpression
* @param {string} filePath
* @returns {string}
*/
buildMessage(callExpression, filePath) {
const location = callExpression.callee.loc
const start = location.start.line
const end = location.end.line
return `The usage in file "${filePath}" at line ${start}:${end} could not be transformed into the new syntax. Please do this manually.`
}
}
module.exports = UnknownUsageError

View File

@ -0,0 +1,55 @@
module.exports = (file, api) => {
const jscodeshift = api.jscodeshift
const root = jscodeshift(file.source)
const importSpecifiers = root
.find(jscodeshift.ImportDeclaration, {
source: {
value: '@tanstack/react-query',
},
})
.find(jscodeshift.ImportSpecifier, {
imported: {
name: 'Hydrate',
},
})
if (importSpecifiers.length > 0) {
const names = {
searched: 'Hydrate', // By default, we want to replace the `Hydrate` usages.
target: 'HydrationBoundary', // We want to replace them with `HydrationBoundary`.
}
importSpecifiers.replaceWith(({ node: mutableNode }) => {
/**
* When the local and imported names match which means the code doesn't contain import aliases, we need
* to replace only the import specifier.
* @type {boolean}
*/
const usesDefaultImport =
mutableNode.local.name === mutableNode.imported.name
if (!usesDefaultImport) {
// If the code uses import aliases, we must re-use the alias.
names.searched = mutableNode.local.name
names.target = mutableNode.local.name
}
// Override the import specifier.
mutableNode.imported.name = 'HydrationBoundary'
return mutableNode
})
root
.findJSXElements(names.searched)
.replaceWith(({ node: mutableNode }) => {
mutableNode.openingElement.name.name = names.target
mutableNode.closingElement.name.name = names.target
return mutableNode
})
}
return root.toSource({ quote: 'single', lineTerminator: '\n' })
}

View File

@ -0,0 +1,41 @@
module.exports = (file, api) => {
const jscodeshift = api.jscodeshift
const root = jscodeshift(file.source)
const baseRenameLogic = (kind, from, to) => {
root
.find(kind, (node) => {
return (
node.computed === false &&
(node.key?.name === from || node.key?.value === from)
)
})
.replaceWith(({ node: mutableNode }) => {
if (mutableNode.key.name !== undefined) {
mutableNode.key.name = to
}
if (mutableNode.key.value !== undefined) {
mutableNode.key.value = to
}
return mutableNode
})
}
const renameObjectProperty = (from, to) => {
baseRenameLogic(jscodeshift.ObjectProperty, from, to)
}
const renameTypeScriptPropertySignature = (from, to) => {
baseRenameLogic(jscodeshift.TSPropertySignature, from, to)
}
renameObjectProperty('cacheTime', 'gcTime')
renameObjectProperty('useErrorBoundary', 'throwOnError')
renameTypeScriptPropertySignature('cacheTime', 'gcTime')
renameTypeScriptPropertySignature('useErrorBoundary', 'throwOnError')
return root.toSource({ quote: 'single', lineTerminator: '\n' })
}