vue-declassify
Version:
Convert Vue 2 class-based components into object-based syntax.
241 lines (240 loc) • 9.89 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.extract = void 0;
const ts_morph_1 = require("ts-morph");
const tiny_case_1 = require("tiny-case");
function unpackComponentDecorator(decorator) {
const decoratorArguments = decorator.getArguments();
if (decoratorArguments.length > 0) {
const initialDecoratorArgument = decoratorArguments[0];
if (!(initialDecoratorArgument instanceof ts_morph_1.ObjectLiteralExpression)) {
throw new Error('The first argument to @Component is not an object literal.');
}
return {
properties: initialDecoratorArgument.getProperties(),
};
}
return {
properties: [],
};
}
function unpackPropDecorator(decorator, argumentIndex = 0) {
const configuration = {};
const decoratorArguments = decorator.getArguments();
if (decoratorArguments.length > 0) {
const propOptionsArgument = decoratorArguments[argumentIndex];
if (!(propOptionsArgument instanceof ts_morph_1.ObjectLiteralExpression)) {
throw new Error('The first argument to @Prop is not an object literal.');
}
const requiredProperty = propOptionsArgument.getProperty('required');
if (requiredProperty) {
if (!(requiredProperty instanceof ts_morph_1.PropertyAssignment)) {
throw new Error('The `required` value to @Prop is not a property assignment.');
}
configuration.required = requiredProperty;
}
const defaultProperty = propOptionsArgument.getProperty('default');
if (defaultProperty) {
if (!(defaultProperty instanceof ts_morph_1.PropertyAssignment)) {
throw new Error('The `default` value to @Prop is not a property assignment.');
}
configuration.default = defaultProperty;
}
}
return configuration;
}
function unpackPropSyncDecorator(decorator) {
const decoratorArguments = decorator.getArguments();
if (decoratorArguments.length === 0) {
throw new Error('@PropSync does not have at least its first argument.');
}
const syncPathArgument = decoratorArguments[0];
if (!(syncPathArgument instanceof ts_morph_1.StringLiteral)) {
throw new Error('The first argument to @PropSync is not a string literal.');
}
return Object.assign({ sync: syncPathArgument.getLiteralValue() }, unpackPropDecorator(decorator, 1));
}
function unpackWatchDecorator(decorator) {
const decoratorArguments = decorator.getArguments();
if (decoratorArguments.length === 0) {
throw new Error('@Watch does not at least its first argument.');
}
const watchPathArgument = decoratorArguments[0];
if (!(watchPathArgument instanceof ts_morph_1.StringLiteral)) {
throw new Error('The first argument to @Watch is not a string literal.');
}
const configuration = {
path: watchPathArgument.getLiteralValue(),
};
if (decoratorArguments.length > 1) {
const watchOptionsArgument = decoratorArguments[1];
if (!(watchOptionsArgument instanceof ts_morph_1.ObjectLiteralExpression)) {
throw new Error('The second argument to @Watch is not an object literal.');
}
const deepProperty = watchOptionsArgument.getProperty('deep');
if (deepProperty) {
if (!(deepProperty instanceof ts_morph_1.PropertyAssignment)) {
throw new Error('The `deep` property to @Watch is not a property assignment.');
}
configuration.deep = deepProperty.getText();
}
const immediateProperty = watchOptionsArgument.getProperty('immediate');
if (immediateProperty) {
if (!(immediateProperty instanceof ts_morph_1.PropertyAssignment)) {
throw new Error('The `immediate` property to @Watch is not a property assignment.');
}
configuration.immediate = immediateProperty.getText();
}
}
return configuration;
}
// Immediately rewrites @Emit by appending it to the end of its function.
function rewriteEmitDecorator(method, decorator) {
const [nameLiteral] = decorator.getArguments();
// The name for @Emit is either the decorator's first argument, or defaults to the method name.
let eventName;
if (nameLiteral) {
if (!(nameLiteral instanceof ts_morph_1.StringLiteral)) {
throw new Error('The first argument to @Emit must be a string literal.');
}
eventName = nameLiteral.getLiteralValue();
}
else {
eventName = method.getName();
// Per the documentation, when using the function as the event name, it is rewritten using kebab-case.
eventName = tiny_case_1.kebabCase(eventName);
}
// Determine which return statements are top-level by iteratively looking at their parent nodes.
const toplevelReturns = method
.getDescendantsOfKind(ts_morph_1.SyntaxKind.ReturnStatement)
.filter(statement => {
let parent = statement.getParent();
while (parent) {
if (ts_morph_1.Node.isScopedNode(parent)) {
// The definition of top-level: the closest scoped parent node is the decorated method.
return parent === method;
}
parent = parent.getParent();
}
return false;
});
if (toplevelReturns.length > 0) {
method.setIsAsync(true);
for (const statement of toplevelReturns) {
if (statement.wasForgotten()) {
continue;
}
const expression = statement.getExpressionOrThrow();
statement.replaceWithText(`this.$emit('${eventName}', await ${expression.getText()})\nreturn`);
}
return;
}
const parameters = method
.getParameters()
.map(parameter => parameter.getName())
.join(', ');
if (parameters) {
method.setBodyText(`${method.getBodyText()}\nthis.$emit('${eventName}', ${parameters})`);
}
else {
method.setBodyText(`${method.getBodyText()}\nthis.$emit('${eventName}')`);
}
}
// Unpacks a Vue class declaration into its Vue properties.
function unpackClass(declaration) {
const props = [];
const syncProps = [];
const data = [];
const methods = [];
const computed = {};
const watches = [];
let vmodel;
for (const property of declaration.getInstanceProperties()) {
if (property instanceof ts_morph_1.PropertyDeclaration) {
{
const decorator = property.getDecorator('VModel');
if (decorator) {
if (vmodel) {
throw new Error('Multiple @VModel properties were detected.');
}
props.push(Object.assign(Object.assign({ declaration: property }, unpackPropDecorator(decorator)), { vmodel: true }));
vmodel = property;
continue; // Processed it, so continue.
}
}
{
const decorator = property.getDecorator('Prop');
if (decorator) {
props.push(Object.assign(Object.assign({ declaration: property }, unpackPropDecorator(decorator)), { vmodel: false }));
continue; // Processed it, so continue.
}
}
{
const decorator = property.getDecorator('PropSync');
if (decorator) {
syncProps.push(Object.assign({ declaration: property }, unpackPropSyncDecorator(decorator)));
continue; // Processed it, so continue.
}
}
// Undecorated property, so it's plain old data.
data.push(property);
}
else if (property instanceof ts_morph_1.GetAccessorDeclaration) {
const name = property.getName();
if (!(name in computed)) {
computed[name] = {};
}
computed[name].getter = property;
}
else if (property instanceof ts_morph_1.SetAccessorDeclaration) {
const name = property.getName();
if (!(name in computed)) {
computed[name] = {};
}
computed[name].setter = property;
}
else {
throw new Error(`Unexpected instance member of type: ${property.getKindName()}.`);
}
}
for (const method of declaration.getInstanceMethods()) {
for (const decorator of method.getDecorators()) {
if (decorator.getName() === 'Watch') {
watches.push(Object.assign({ method: method.getName() }, unpackWatchDecorator(decorator)));
decorator.remove();
}
else if (decorator.getName() === 'Emit') {
rewriteEmitDecorator(method, decorator);
decorator.remove();
}
}
methods.push(method);
}
return {
props,
syncProps,
data,
computed,
methods,
watches,
vmodel,
};
}
// Extracts all Vue properties from the Vue class in a source file.
// Mostly responsible for implementing the various decorators.
function extract(source) {
const defaultExport = source.getDefaultExportSymbol();
if (!defaultExport) {
return;
}
const declaration = defaultExport.getValueDeclaration();
if (!(declaration instanceof ts_morph_1.ClassDeclaration)) {
return;
}
const decorator = declaration.getDecorator('Component');
if (!decorator) {
return;
}
return Object.assign(Object.assign({ declaration }, unpackClass(declaration)), { decorator: Object.assign({}, unpackComponentDecorator(decorator)) });
}
exports.extract = extract;