vue-declassify
Version:
Convert Vue 2 class-based components into object-based syntax.
433 lines (432 loc) • 13.3 kB
JavaScript
;
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;