Skip to content

Instantly share code, notes, and snippets.

@mojabyte
Created February 16, 2024 15:43
Show Gist options
  • Save mojabyte/5da09956530b80a7cdedea4e34f930a0 to your computer and use it in GitHub Desktop.
Save mojabyte/5da09956530b80a7cdedea4e34f930a0 to your computer and use it in GitHub Desktop.
Vue 3 codemod: Options API to Composition API and Script Setup
// 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