UNPKG

vue-declassify

Version:

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

433 lines (432 loc) 13.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.classToObject = void 0; const vue_class = __importStar(require("./vue_class")); const imports = __importStar(require("./imports")); const LIFECYCLE_HOOKS = Object.freeze([ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', ]); function writeDocs(writer, docs) { for (const doc of docs) { writer.writeLine('/**'); for (const line of doc.getInnerText().split('\n')) { writer .write(' *') .conditionalWrite(!!line.trim(), ' ') .write(line) .newLine(); } writer.writeLine(' */'); } } function writeName(writer, declaration) { writer .write('name:') .space() .quote() .write(declaration.getNameOrThrow()) .quote() .write(',') .newLine(); } function writeConfig(writer, decorator) { if (decorator.properties.length > 0) { for (const property of decorator.properties) { writer.write(property.getText()); } writer .write(',') .newLine(); } } function writeProps(writer, props) { const callbacks = []; if (props.length > 0) { writer .write('props:') .space() .write('{') .newLine() .withIndentationLevel(1, () => { for (let prop of props) { callbacks.push(...writeProp(writer, prop)); } }) .writeLine('},'); } return callbacks; } function writeProp(writer, prop) { const callbacks = []; writeDocs(writer, prop.declaration.getJsDocs()); let name = prop.declaration.getName(); if (prop.vmodel) { name = 'value'; } writer .write(`${name}:`) .space() .write('{') .withIndentationLevel(1, () => { callbacks.push(...writePropType(writer, prop.declaration)); writePropOptions(writer, prop); }) .writeLine('},'); return callbacks; } function writePropType(writer, declaration) { const callbacks = []; const type = declaration.getType(); writer.write('type: '); if (type.isString()) { writer.write('String'); } else if (type.isNumber()) { writer.write('Number'); } else if (type.isBoolean()) { writer.write('Boolean'); } else { const actualType = declaration.getTypeNodeOrThrow().getText(); // Vue.js props can only be primitive types, unless you use PropType. // However, even when using PropType, the base annotated type must be // the same type as the annotated one, or you get type errors anyway. let baseType; // HACK: Adjust Object/Function/Array based on what the type seems to be. // This heuristic can be improved drastically, and is part of what makes // a project like vue-declassify difficult. if (type.getCallSignatures().length > 0) { // This one is actually pretty safe. TS will tell us if what's inside has // a call signature, making it a function. baseType = 'Function'; } else if (actualType.startsWith('Array<') || actualType.endsWith('[]')) { // This is some nonsense calculation but, it's quite effective? Arrays // are easy to spot syntactically. This doesn't work for user-defined // array types though. Fortunately, those are exceedingly rare. baseType = 'Array'; } else { baseType = 'Object'; } writer.write(`${baseType} as PropType<${actualType}>`); // Add PropType to the imports afterwards, since we just used it. callbacks.push(source => { imports.ensure(source, 'vue', { named: ['PropType'], }); }); } writer .write(',') .newLine(); return callbacks; } function writePropOptions(writer, options) { // Only permit exactly one of `default` and `required`, // since a default value implies required is false in Vue. // There actually doesn't seem to be a use-case to set both! if (options.default) { writer.write(options.default.getText()); } else if (options.required) { writer.write(options.required.getText()); } else if (options.declaration.hasExclamationToken()) { // Allow ! to indicate implicitly that the prop is required. writer.write('required: true'); } else { // Lastly, if neither property is directly supplied, mark `required` false. writer.write('required: false'); } writer .write(',') .newLine(); } function writeData(writer, data) { if (data.length > 0) { writer .writeLine('data()') .space() .write('{') .newLine() .withIndentationLevel(1, () => { writer.writeLine('return {'); for (const property of data) { writeDataProperty(writer, property); } writer.writeLine('};'); }) .writeLine('},'); } } function writeDataProperty(writer, property) { writeDocs(writer, property.getJsDocs()); writer .write(property.getName()) .write(':') .space() .write(property.getInitializerOrThrow().getText()); const type = property.getTypeNode(); if (type) { writer .space() .write('as') .space() .write(type.getText()); } writer .write(',') .newLine(); } function writeComputed(writer, vmodel, computed) { if (vmodel || Object.keys(computed).length > 0) { writer .write('computed:') .space() .write('{') .newLine(); if (vmodel) { writeComputedVModelProperty(writer, vmodel); } for (const [name, { getter, setter }] of Object.entries(computed)) { if (getter) { if (!setter) { writeComputedGetter(writer, getter); } else { writeComputedProperty(writer, name, getter, setter); } } } writer.writeLine('},'); } } function writeComputedVModelProperty(writer, property) { writer .write(property.getName()) .write(':') .space() .write('{') .newLine() .withIndentationLevel(1, () => { writer .write(`get() {`) .newLine() .withIndentationLevel(1, () => { writer .write('return this.value') .newLine(); }) .writeLine('},'); writer .write('set(value) {') .newLine() .withIndentationLevel(1, () => { writer .write('this.$emit(\'input\', value)') .newLine(); }) .writeLine('},'); }) .writeLine('},'); } function writeComputedProperty(writer, name, getter, setter) { writer .write(name) .write(':') .space() .write('{') .newLine() .withIndentationLevel(1, () => { var _a, _b; const setParameter = setter.getParameters()[0]; if (!setParameter) { throw new Error('Computed setter doesn\'t seem to have a parameter.'); } writeDocs(writer, getter.getJsDocs()); writer .write(`get()`) .write(':') .space() // Computed property getters need to match the setter's return type, // But there's actually a variety of places this can be obtained... // try them all before giving up with `any`. .write(((_a = getter.getReturnTypeNode()) === null || _a === void 0 ? void 0 : _a.getText()) || ((_b = setParameter.getTypeNode()) === null || _b === void 0 ? void 0 : _b.getText()) || 'any') .newLine() .write(getter.getBodyOrThrow().getText()) .write(',') .newLine(); writeDocs(writer, setter.getJsDocs()); writer .write('set(') .write(setParameter.getText()) .write(')') .newLine() .write(setter.getBodyOrThrow().getText()) .write(',') .newLine(); }) .writeLine('},'); } function writeComputedGetter(writer, getter) { var _a; writeDocs(writer, getter.getJsDocs()); writer .write(`${getter.getName()}():`) .space() .write(((_a = getter.getReturnTypeNode()) === null || _a === void 0 ? void 0 : _a.getText()) || 'any') .newLine() .write(getter.getBodyOrThrow().getText()) .write(',') .newLine(); } function writeMethods(writer, methods) { const lifecycleMethods = []; const normalMethods = []; for (const method of methods) { if (LIFECYCLE_HOOKS.includes(method.getName())) { lifecycleMethods.push(method); } else { normalMethods.push(method); } } for (const method of lifecycleMethods) { writeMethod(writer, method); } if (normalMethods.length > 0) { writer .write('methods:') .space() .write('{') .newLine() .withIndentationLevel(1, () => { for (const method of normalMethods) { writeMethod(writer, method); } }) .writeLine('},'); } } function writeMethod(writer, method) { writeDocs(writer, method.getJsDocs()); writer .write(method.getText()) .write(',') .newLine(); } function writeWatches(writer, watches) { if (watches.length > 0) { writer .write('watch:') .space() .write('{') .newLine() .withIndentationLevel(1, () => { for (const watch of watches) { writeWatch(writer, watch); } }) .writeLine('},'); } } function writeWatch(writer, watch) { writer .quote() .write(watch.path) .quote() .write(':') .space() .write('{') .newLine() .withIndentationLevel(1, () => { writer .write('// @ts-ignore') .newLine() .write('handler:') .space() .write(`'${watch.method}'`) .write(',') .newLine(); if (watch.immediate) { writer .write(watch.immediate) .write(',') .newLine(); } if (watch.deep) { writer .write(watch.deep) .write(',') .newLine(); } }) .writeLine('},'); } function classToObject(source) { const vue = vue_class.extract(source); if (!vue) { return; } const { declaration, decorator, props, data, computed, methods, syncProps, watches, vmodel, } = vue; const callbacks = [ source => source.formatText() ]; source.addExportAssignment({ leadingTrivia: writer => { writeDocs(writer, declaration.getJsDocs()); }, expression: writer => { writer .writeLine('Vue.extend({') .withIndentationLevel(1, () => { writeName(writer, declaration); writeConfig(writer, decorator); callbacks.push(...writeProps(writer, props)); writeData(writer, data); writeComputed(writer, vmodel, computed); writeWatches(writer, watches); writeMethods(writer, methods); }) .write('})'); }, isExportEquals: false, }); // Perform any processing that had to happen after we finished writing. for (const callback of callbacks.reverse()) { callback(source); } // Remove the class now that we're done reading everything. vue.declaration.remove(); } exports.classToObject = classToObject;