From 2714c6b55b823030c3859321a0aa6562f0c8fb0c Mon Sep 17 00:00:00 2001 From: yuangongji Date: Fri, 7 May 2021 21:07:15 +0800 Subject: [PATCH 1/6] feat: add emits declaration --- .../codemods/vue/add-emit-declaration.js | 48 +++++++++++++++++++ generator/codemods/vue/index.js | 12 +++++ generator/index.js | 8 ++++ 3 files changed, 68 insertions(+) create mode 100644 generator/codemods/vue/add-emit-declaration.js create mode 100644 generator/codemods/vue/index.js diff --git a/generator/codemods/vue/add-emit-declaration.js b/generator/codemods/vue/add-emit-declaration.js new file mode 100644 index 0000000..984a309 --- /dev/null +++ b/generator/codemods/vue/add-emit-declaration.js @@ -0,0 +1,48 @@ +/** + * @param {Object} context + * @param {import('jscodeshift').JSCodeshift} context.j + * @param {ReturnType} context.root + */ +module.exports = function addEmitDeclaration(context) { + const { j, root } = context + + // this.$emit('xxx') => emits: ['xxx'] + const this$emits = root.find(j.CallExpression, { + callee: { + type: 'MemberExpression', + object: { type: 'ThisExpression' }, + property: { + type: 'Identifier', + name: '$emit' + } + } + }) + + const emits = [] + for (let i = 0; i < this$emits.length; i++) { + const arg = this$emits.at(i).get().node.arguments[0] + if (arg.type === 'StringLiteral') { + emits.push(arg.value) + } + } + + if (emits.length === 0) { + return + } + + const properties = root + .find(j.ExportDefaultDeclaration) + .at(0) + .find(j.ObjectExpression) + .at(0) + + properties.replaceWith(nodePath => { + nodePath.node.properties.unshift( + j.objectProperty( + j.identifier('emits'), + j.arrayExpression(emits.map(el => j.stringLiteral(el))) + ) + ) + return nodePath.node + }) +} diff --git a/generator/codemods/vue/index.js b/generator/codemods/vue/index.js new file mode 100644 index 0000000..b6aa4ed --- /dev/null +++ b/generator/codemods/vue/index.js @@ -0,0 +1,12 @@ +/** @type {import('jscodeshift').Transform} */ +module.exports = function(fileInfo, api) { + const j = api.jscodeshift + const root = j(fileInfo.source) + const context = { j, root } + + require('./add-emit-declaration')(context) + + return root.toSource({ lineTerminator: '\n' }) +} + +module.exports.parser = 'babylon' diff --git a/generator/index.js b/generator/index.js index d9f4604..e97a555 100644 --- a/generator/index.js +++ b/generator/index.js @@ -22,6 +22,14 @@ module.exports = (api) => { const globalAPITransform = require('./codemods/global-api') api.transformScript(api.entryFile, globalAPITransform) + const vueTransform = require('./codemods/vue') + const vueFiles = Object.keys(api.generator.files).filter(el => + el.endsWith('.vue') + ) + for (let i = 0; i < vueFiles.length; i++) { + api.transformScript(vueFiles[i], vueTransform) + } + if (api.hasPlugin('eslint')) { api.extendPackage({ devDependencies: { From c5aba79fc1b1654e7c7095ef4de8d5a651578043 Mon Sep 17 00:00:00 2001 From: yuangongji Date: Mon, 10 May 2021 21:03:52 +0800 Subject: [PATCH 2/6] add deep option for watch --- generator/codemods/vue/add-watch-deep.js | 124 +++++++++++++++++++++++ generator/codemods/vue/index.js | 1 + 2 files changed, 125 insertions(+) create mode 100644 generator/codemods/vue/add-watch-deep.js diff --git a/generator/codemods/vue/add-watch-deep.js b/generator/codemods/vue/add-watch-deep.js new file mode 100644 index 0000000..ffeb9e5 --- /dev/null +++ b/generator/codemods/vue/add-watch-deep.js @@ -0,0 +1,124 @@ +/** + * @param {Object} context + * @param {import('jscodeshift').JSCodeshift} context.j + * @param {ReturnType} context.root + */ +module.exports = function addEmitDeclaration(context) { + const { j, root } = context + + // this.$watch(...) add deep option + const this$watches = root.find(j.CallExpression, { + callee: { + type: 'MemberExpression', + object: { type: 'ThisExpression' }, + property: { + type: 'Identifier', + name: '$watch' + } + } + }) + + for (let i = 0; i < this$watches.length; i++) { + const watchFunc = this$watches.at(i) + const deepProperty = watchFunc.find(j.ObjectProperty, { + key: { + type: 'Identifier', + name: 'deep' + } + }) + if (deepProperty.length > 0) { + continue + } + const arguments = watchFunc.get().node.arguments + if (arguments.length < 2) { + continue + } + if (arguments[1].type != 'ObjectExpression') { + if (arguments.length < 3) { + watchFunc.replaceWith(nodePath => { + nodePath.node.arguments.push(j.objectExpression([])) + return nodePath.node + }) + } + const target = watchFunc.find(j.ObjectExpression).at(0) + target.replaceWith(nodePath => { + nodePath.node.properties.push( + j.objectProperty(j.identifier('deep'), j.booleanLiteral(true)) + ) + return nodePath.node + }) + } + } + + // watch: {...} add deep option + const watchFuncs = root + .find(j.ExportDefaultDeclaration) + .at(0) + .find(j.ObjectExpression) + .at(0) + .find(j.ObjectProperty, { + key: { + type: 'Identifier', + name: 'watch' + } + }) + .at(0) + .find(j.ObjectExpression) + .at(0) + .find(j.ObjectProperty) + + for (let i = 0; i < watchFuncs.length; i++) { + const watchProperty = watchFuncs.at(i) + if (!inExportDefaultLevel(watchProperty, 2)) { + continue + } + const deepProperty = watchProperty.find(j.ObjectProperty, { + key: { + type: 'Identifier', + name: 'deep' + } + }) + if (deepProperty.length > 0) { + continue + } + + if (watchProperty.get().node.value.type === 'ObjectExpression') { + const target = watchProperty.find(j.ObjectExpression).at(0) + target.replaceWith(nodePath => { + nodePath.node.properties.push( + j.objectProperty(j.identifier('deep'), j.booleanLiteral(true)) + ) + return nodePath.node + }) + } else { + watchProperty.replaceWith(nodePath => { + nodePath.node.value = j.objectExpression([ + j.objectProperty(j.identifier('handler'), nodePath.node.value), + j.objectProperty(j.identifier('deep'), j.booleanLiteral(true)) + ]) + return nodePath.node + }) + } + } +} + +function getExportDefaultLevel(collection) { + let path = collection.get() + let level = 0 + while (path) { + if (path.node.type === 'ExportDefaultDeclaration') { + return level + } + path = path.parentPath + level++ + } + return -1 +} + +function inExportDefaultLevel(collection, level) { + const lvl = getExportDefaultLevel(collection) + if (level * 3 === lvl) { + return true + } + return false +} diff --git a/generator/codemods/vue/index.js b/generator/codemods/vue/index.js index b6aa4ed..d51f32e 100644 --- a/generator/codemods/vue/index.js +++ b/generator/codemods/vue/index.js @@ -5,6 +5,7 @@ module.exports = function(fileInfo, api) { const context = { j, root } require('./add-emit-declaration')(context) + require('./add-watch-deep')(context) return root.toSource({ lineTerminator: '\n' }) } From da1dbd83f5131a01375715f62800ec9fba8f3013 Mon Sep 17 00:00:00 2001 From: ygj6 Date: Tue, 11 May 2021 20:18:21 +0800 Subject: [PATCH 3/6] feet: remove event native; add transition from --- generator/codemods/vue-addition/index.js | 28 ++++++++++++++++++++++++ generator/index.js | 12 ++++++++++ 2 files changed, 40 insertions(+) create mode 100644 generator/codemods/vue-addition/index.js diff --git a/generator/codemods/vue-addition/index.js b/generator/codemods/vue-addition/index.js new file mode 100644 index 0000000..918fa38 --- /dev/null +++ b/generator/codemods/vue-addition/index.js @@ -0,0 +1,28 @@ +module.exports = function(files, filename) { + let content = files[filename] + content = removeEventNative(content) + content = addTransitionFrom(content) + files[filename] = content +} + +// template +// v-on:event.native => v-on:event +// @event.native => @event +function removeEventNative(content) { + const reg = new RegExp( + '(?<=)', + 'g' + ) + return content.replace(reg, '') +} + +// style +// .xxx-enter => .xxx-enter-from +// .xxx-leave => .xxx-leave-from +function addTransitionFrom(content) { + const reg = new RegExp( + '(?<=][\\s\\S]*?\\s\\.[A-Za-z0-9_-]+-)(enter|leave)(?=[,{\\s][\\s\\S]*?)', + 'g' + ) + return content.replace(reg, '$1-from') +} diff --git a/generator/index.js b/generator/index.js index e97a555..f6d2480 100644 --- a/generator/index.js +++ b/generator/index.js @@ -134,3 +134,15 @@ module.exports = (api) => { api.exitLog('Documentation available at https://github.com/vuejs/vue-test-utils-next') } } + +module.exports.hooks = api => { + api.postProcessFiles(files => { + const vueTransform = require('./codemods/vue-addition') + const vueFiles = Object.keys(api.generator.files).filter(el => + el.endsWith('.vue') + ) + for (let i = 0; i < vueFiles.length; i++) { + vueTransform(files, vueFiles[i]) + } + }) +} From 3f5d2f35527f87821003aaf5bd9712d7450dda09 Mon Sep 17 00:00:00 2001 From: ygj6 Date: Wed, 12 May 2021 11:36:35 +0800 Subject: [PATCH 4/6] feat: implement tree-shaking api --- generator/codemods/global-api/index.js | 3 +++ generator/codemods/global-api/next-tick.js | 30 +++++++++++++++++++++ generator/codemods/global-api/observable.js | 30 +++++++++++++++++++++ generator/codemods/global-api/version.js | 29 ++++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 generator/codemods/global-api/next-tick.js create mode 100644 generator/codemods/global-api/observable.js create mode 100644 generator/codemods/global-api/version.js diff --git a/generator/codemods/global-api/index.js b/generator/codemods/global-api/index.js index 6ab3557..9d3720e 100644 --- a/generator/codemods/global-api/index.js +++ b/generator/codemods/global-api/index.js @@ -11,6 +11,9 @@ module.exports = function(fileInfo, api) { require('./remove-production-tip')(context) require('./remove-vue-use')(context) require('./remove-contextual-h')(context) + require('./next-tick')(context) + require('./observable')(context) + require('./version')(context) // remove extraneous imports const removeExtraneousImport = require('../utils/remove-extraneous-import') diff --git a/generator/codemods/global-api/next-tick.js b/generator/codemods/global-api/next-tick.js new file mode 100644 index 0000000..f713005 --- /dev/null +++ b/generator/codemods/global-api/next-tick.js @@ -0,0 +1,30 @@ +/** + * @param {Object} context + * @param {import('jscodeshift').JSCodeshift} context.j + * @param {ReturnType} context.root + */ +module.exports = function createAppMount(context) { + const { j, root } = context + + // Vue.nextTick(() => {}) + const nextTickCalls = root.find(j.CallExpression, n => { + return ( + n.callee.type === 'MemberExpression' && + n.callee.property.name === 'nextTick' && + n.callee.object.name === 'Vue' + ) + }) + + if (!nextTickCalls.length) { + return + } + + const addImport = require('../utils/add-import') + addImport(context, { imported: 'nextTick' }, 'vue') + + nextTickCalls.replaceWith(({ node }) => { + const el = node.arguments[0] + + return j.callExpression(j.identifier('nextTick'), [el]) + }) +} diff --git a/generator/codemods/global-api/observable.js b/generator/codemods/global-api/observable.js new file mode 100644 index 0000000..9a01221 --- /dev/null +++ b/generator/codemods/global-api/observable.js @@ -0,0 +1,30 @@ +/** + * @param {Object} context + * @param {import('jscodeshift').JSCodeshift} context.j + * @param {ReturnType} context.root + */ +module.exports = function createAppMount(context) { + const { j, root } = context + + // Vue.observable(state) + const observableCalls = root.find(j.CallExpression, n => { + return ( + n.callee.type === 'MemberExpression' && + n.callee.property.name === 'observable' && + n.callee.object.name === 'Vue' + ) + }) + + if (!observableCalls.length) { + return + } + + const addImport = require('../utils/add-import') + addImport(context, { imported: 'reactive' }, 'vue') + + observableCalls.replaceWith(({ node }) => { + const el = node.arguments[0] + + return j.callExpression(j.identifier('reactive'), [el]) + }) +} diff --git a/generator/codemods/global-api/version.js b/generator/codemods/global-api/version.js new file mode 100644 index 0000000..fa8e894 --- /dev/null +++ b/generator/codemods/global-api/version.js @@ -0,0 +1,29 @@ +/** + * @param {Object} context + * @param {import('jscodeshift').JSCodeshift} context.j + * @param {ReturnType} context.root + */ +module.exports = function createAppMount(context) { + const { j, root } = context + + // Vue.version + const versionCalls = root.find(j.MemberExpression, n => { + return ( + n.property.name === 'version' && + n.object.name === 'Vue' + ) + }) + + if (!versionCalls.length) { + return + } + + const addImport = require('../utils/add-import') + addImport(context, { imported: 'version' }, 'vue') + + versionCalls.replaceWith(({ node }) => { + const property = node.property.name + + return j.identifier(property) + }) +} From 19d6cee42a7ea73888f7314ddb1e4e7d09e27486 Mon Sep 17 00:00:00 2001 From: ygj6 Date: Wed, 12 May 2021 11:50:33 +0800 Subject: [PATCH 5/6] feat: rename deprecated lifecycle --- generator/codemods/vue/index.js | 1 + generator/codemods/vue/rename-lifecycle.js | 28 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 generator/codemods/vue/rename-lifecycle.js diff --git a/generator/codemods/vue/index.js b/generator/codemods/vue/index.js index d51f32e..b0d67ee 100644 --- a/generator/codemods/vue/index.js +++ b/generator/codemods/vue/index.js @@ -6,6 +6,7 @@ module.exports = function(fileInfo, api) { require('./add-emit-declaration')(context) require('./add-watch-deep')(context) + require('./rename-lifecycle')(context) return root.toSource({ lineTerminator: '\n' }) } diff --git a/generator/codemods/vue/rename-lifecycle.js b/generator/codemods/vue/rename-lifecycle.js new file mode 100644 index 0000000..48f3673 --- /dev/null +++ b/generator/codemods/vue/rename-lifecycle.js @@ -0,0 +1,28 @@ +/** @type {import('jscodeshift').Transform} */ + +const DEPRECATED_LIFECYCLE = Object.create(null) +DEPRECATED_LIFECYCLE.destroyed = 'unmounted' +DEPRECATED_LIFECYCLE.beforeDestroy = 'beforeUnmount' + +module.exports = function renameLifecycle(context) { + const { j, root } = context + + const renameDeprecatedLifecycle = path => { + const name = path.node.key.name + + if ( + DEPRECATED_LIFECYCLE[name] && + path.parent && + path.parent.parent && + path.parent.parent.value.type === 'ExportDefaultDeclaration' + ) { + path.value.key.name = DEPRECATED_LIFECYCLE[name] + } + } + + root.find(j.ObjectProperty).forEach(renameDeprecatedLifecycle) + root.find(j.ObjectMethod).forEach(renameDeprecatedLifecycle) + root.find(j.ClassProperty).forEach(renameDeprecatedLifecycle) + + return root.toSource({ lineTerminator: '\n' }) +} From abd49fddeba992bd1560068574f3af945c6da76e Mon Sep 17 00:00:00 2001 From: ygj6 Date: Wed, 12 May 2021 15:31:11 +0800 Subject: [PATCH 6/6] Feature: v-model support. --- generator/codemods/vue/index.js | 1 + generator/codemods/vue/vModel.js | 92 ++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 generator/codemods/vue/vModel.js diff --git a/generator/codemods/vue/index.js b/generator/codemods/vue/index.js index b0d67ee..1f9f68f 100644 --- a/generator/codemods/vue/index.js +++ b/generator/codemods/vue/index.js @@ -7,6 +7,7 @@ module.exports = function(fileInfo, api) { require('./add-emit-declaration')(context) require('./add-watch-deep')(context) require('./rename-lifecycle')(context) + require('./vModel')(context) return root.toSource({ lineTerminator: '\n' }) } diff --git a/generator/codemods/vue/vModel.js b/generator/codemods/vue/vModel.js new file mode 100644 index 0000000..ab748bc --- /dev/null +++ b/generator/codemods/vue/vModel.js @@ -0,0 +1,92 @@ +module.exports = function addEmitDeclaration(context) { + const { + j, + root + } = context; + const vModel = root + .find(j.ObjectProperty, { + key: { + name: 'model' + } + }) + .filter(p => p.parentPath.parentPath.parentPath.node.type == 'ExportDefaultDeclaration') + .at(0); + + if (!vModel.length) return + const propNd = vModel.find(j.ObjectProperty, { + key: { + name: 'prop' + } + }).get(); + const eventNd = vModel.find(j.ObjectProperty, { + key: { + name: 'event' + } + }).get(); + const propName = propNd.node.value.value + const propEvent = eventNd.node.value.value + + const props = root + .find(j.ObjectProperty, { + key: { + name: 'props' + } + }) + .filter(p => p.parentPath.parentPath.parentPath.node.type == 'ExportDefaultDeclaration') + .at(0); + + if (!props.length) return + const valueNd = props.find(j.ObjectProperty, { + key: { + name: 'value' + } + }) + .filter(p => p.parentPath.parentPath.parentPath == props.get()) + .get(); + + valueNd.node.key.name = 'modelValue' + + let methods = root + .find(j.ObjectProperty, { + key: { + name: 'methods' + } + }) + .filter(p => p.parentPath.parentPath.parentPath.node.type == 'ExportDefaultDeclaration') + .at(0); + + + if (!methods.length) { + vModel.get().parentPath.value.push( + j.objectProperty(j.identifier('methods'), j.objectExpression([])) + ) + } + methods = root + .find(j.ObjectProperty, { + key: { + name: 'methods' + } + }) + .filter(p => p.parentPath.parentPath.parentPath.node.type == 'ExportDefaultDeclaration') + .at(0); + + const funNd = j(` + export default { + ${propEvent}${propName}(${propName}){ + this.$emit('update:modelValue', ${propName}) + }}`).find(j.ObjectMethod).__paths[0].value + + methods.get().node.value.properties.push(funNd) + + vModel.remove() + + + const peopDel = props.find(j.ObjectProperty, { + key: { + name: propName + } + }) + .filter(p => p.parentPath.parentPath.parentPath == props.get()) + .at(0); + peopDel.remove() +} \ No newline at end of file