UNPKG

vue-declassify

Version:

Convert Vue 2 class-based components into object-based syntax.

241 lines (240 loc) 9.89 kB
"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;