Created
February 16, 2024 15:43
-
-
Save mojabyte/5da09956530b80a7cdedea4e34f930a0 to your computer and use it in GitHub Desktop.
Vue 3 codemod: Options API to Composition API and Script Setup
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Usage: pnpm vue-codemod "client/**/*.vue" -t codemods/options-to-composition.cjs | |
// TODO: add support for "data" option | |
const IGNORED_OPTIONS = ['name', 'components']; | |
const LIFECYCLE_METHODS = { | |
created: 'onCreated', | |
beforeMount: 'onBeforeMount', | |
mounted: 'onMounted', | |
beforeUpdate: 'onBeforeUpdate', | |
updated: 'onUpdated', | |
beforeUnmount: 'onBeforeUnmount', | |
unmounted: 'onUnmounted', | |
errorCaptured: 'onErrorCaptured', | |
}; | |
const OPTIONS_ORDER = ['props', 'emits', 'setup', 'computed', 'methods']; | |
module.exports = function (file, api) { | |
const j = api.jscodeshift; | |
let root = j(file.source); | |
function printWarning(message) { | |
console.log(`WARN: ${message} in ${file.path}`); | |
} | |
// Find the ExportDefaultDeclaration node | |
const exportDefaultDeclaration = root.find(j.ExportDefaultDeclaration); | |
// Skip the file if it doesn't have export default | |
if (!exportDefaultDeclaration.length) { | |
return; | |
} | |
// Get the object or function expression from ExportDefaultDeclaration | |
let defaultObject = exportDefaultDeclaration.get('declaration').value; | |
// Extract the object if export is a function call (e.g. defineComponent) | |
if (defaultObject.type === 'CallExpression') { | |
defaultObject = defaultObject.arguments[0]; | |
} | |
// Remember prop and computed names to use for replacing "this" expressions | |
let propNames = []; | |
let computedNames = []; | |
// Transform function for each option property | |
const transformProps = (property) => { | |
// Remember the prop names | |
propNames = property.value.properties.map((property) => property.key.name); | |
return `const props = defineProps(${j(property.value).toSource()})`; | |
}; | |
const transformEmits = (property) => | |
`const emit = defineEmits(${j(property.value).toSource()})`; | |
const transformSetup = (property) => { | |
const returnStatement = j(property) | |
.find(j.ReturnStatement) | |
.filter((path) => { | |
return ( | |
path.parentPath.parentPath.parentPath.parentPath.value.key?.name === | |
'setup' | |
); | |
}); | |
const returnObjectExpression = returnStatement.find(j.ObjectExpression); | |
// Remove the setup return statement | |
returnStatement.remove(); | |
let setupBodySource = j(property.value.body.body).toSource(); | |
if (returnObjectExpression.length) { | |
const objectNode = returnObjectExpression.get().node; | |
// Iterate over the properties of the object | |
const returnVariableDeclarations = objectNode.properties.map( | |
(property) => { | |
if (property.type === 'Property') { | |
const key = property.key.name; | |
if ( | |
property.value.type === 'Identifier' && | |
property.value.loc === null | |
) { | |
return; | |
} | |
return j( | |
j.variableDeclaration('const', [ | |
j.variableDeclarator(j.identifier(key), property.value), | |
]) | |
).toSource(); | |
} | |
printWarning( | |
`Can't transform "${property.type}" in the return of setup function` | |
); | |
return ''; | |
} | |
); | |
setupBodySource = [ | |
...(typeof setupBodySource === 'string' | |
? [setupBodySource] | |
: setupBodySource), | |
...returnVariableDeclarations, | |
]; | |
} | |
return setupBodySource; | |
}; | |
const transformComputed = (property) => { | |
// Remember the computed names | |
computedNames = property.value.properties.map( | |
(property) => property.key.name | |
); | |
// Iterate over properties of the nested object | |
const computedDeclarations = property.value.properties.map( | |
(nestedProperty) => { | |
// Modify the value of each nested property | |
if (!nestedProperty.key) { | |
return; | |
} | |
const key = nestedProperty.key.name || nestedProperty.key.value; | |
const value = nestedProperty.value; | |
const computedExpression = j.callExpression(j.identifier('computed'), [ | |
j.arrowFunctionExpression( | |
value.params, | |
value.body, | |
value.async, | |
value.generator | |
), | |
]); | |
return j( | |
j.variableDeclaration('const', [ | |
j.variableDeclarator(j.identifier(key), computedExpression), | |
]) | |
).toSource(); | |
} | |
); | |
return computedDeclarations.join('\n'); | |
}; | |
const transformMethods = (property) => { | |
const methodDeclarations = property.value.properties.map( | |
(nestedProperty) => { | |
// Modify the value of each nested property | |
if (!nestedProperty.key) { | |
return; | |
} | |
const key = nestedProperty.key.name || nestedProperty.key.value; | |
const value = nestedProperty.value; | |
return j( | |
j.variableDeclaration('const', [ | |
j.variableDeclarator( | |
j.identifier(key), | |
j.arrowFunctionExpression( | |
value.params, | |
value.body, | |
value.async, | |
value.generator | |
) | |
), | |
]) | |
).toSource(); | |
} | |
); | |
return methodDeclarations.join('\n'); | |
}; | |
const transformLifeCycleMethods = (property) => { | |
const methodBodyString = j(property.value).toSource(); | |
return `${LIFECYCLE_METHODS[property.key.name]}(${methodBodyString})`; | |
}; | |
const transformFunctions = { | |
props: transformProps, | |
emits: transformEmits, | |
setup: transformSetup, | |
computed: transformComputed, | |
methods: transformMethods, | |
}; | |
const transformOutputs = {}; | |
defaultObject.properties.forEach((property) => { | |
const key = property.key.name; | |
if (IGNORED_OPTIONS.includes(key)) { | |
return; | |
} | |
if (key in LIFECYCLE_METHODS) { | |
transformOutputs[key] = transformLifeCycleMethods(property); | |
return; | |
} | |
if (key in transformFunctions) { | |
transformOutputs[key] = transformFunctions[key](property); | |
return; | |
} | |
printWarning(`ignoring "${key}" option`); | |
}); | |
// Insert the transformations at the end of the script body | |
OPTIONS_ORDER.forEach((key) => { | |
exportDefaultDeclaration.insertBefore(transformOutputs[key]); | |
}); | |
Object.keys(LIFECYCLE_METHODS).forEach((key) => { | |
exportDefaultDeclaration.insertBefore(transformOutputs[key]); | |
}); | |
// Remove the export default declaration | |
root.find(j.ExportDefaultDeclaration).remove(); | |
// Parse the transformed code | |
try { | |
root = j(root.toSource()); | |
} catch (e) { | |
console.log(`ERROR: ${e} in ${file.path}`); | |
return; | |
} | |
// Extract all "this" expressions | |
const thisExpressions = root.find(j.MemberExpression, { | |
object: { | |
type: 'ThisExpression', | |
}, | |
property: { | |
type: 'Identifier', | |
}, | |
}); | |
// Replace "this" expressions with "props" or computed values | |
thisExpressions.replaceWith((path) => { | |
if (!path.value.property) { | |
return; | |
} | |
const propertyName = path.value.property.name; | |
if (propNames.includes(path.value.property.name)) { | |
return j.memberExpression( | |
j.identifier('props'), | |
j.identifier(propertyName) | |
); | |
} | |
if (computedNames.includes(path.value.property.name)) { | |
return j.memberExpression( | |
j.identifier(propertyName), | |
j.identifier('value') | |
); | |
} | |
printWarning(`Can't replace "this.${propertyName}" expression`); | |
return path.node; | |
}); | |
root = j(root.toSource()); | |
const propsUsages = root | |
.find(j.Identifier, { name: 'props' }) | |
.filter((path) => { | |
return !path.parentPath.value.type.startsWith('VariableDeclarator'); | |
}); | |
// Remove props assignment if there is no usage | |
if (!propsUsages.length) { | |
root.findVariableDeclarators('props').forEach((path) => { | |
const definePropsCall = j.callExpression(j.identifier('defineProps'), [ | |
path.value.init.arguments[0], | |
]); | |
// Replace the variable declaration with the defineProps call | |
j(path.parent).replaceWith(j(definePropsCall).toSource()); | |
}); | |
} | |
const emitUsages = root | |
.find(j.Identifier, { name: 'emit' }) | |
.filter((path) => { | |
return !path.parentPath.value.type.startsWith('VariableDeclarator'); | |
}); | |
// Remove emit assignment if there is no usage | |
if (!emitUsages.length) { | |
root.findVariableDeclarators('emit').forEach((path) => { | |
const defineEmitsCall = j.callExpression(j.identifier('defineEmits'), [ | |
path.value.init.arguments[0], | |
]); | |
// Replace the variable declaration with the defineEmits call | |
j(path.parent).replaceWith(j(defineEmitsCall).toSource()); | |
}); | |
} | |
// Use the transform function to format the code | |
const transformedCode = root.toSource(); | |
return transformedCode; | |
}; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment