Skip to content

Instantly share code, notes, and snippets.

@hidakatsuya
Last active February 28, 2022 15:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hidakatsuya/f1faf5753b6eddf9cf4bc4dae276f181 to your computer and use it in GitHub Desktop.
Save hidakatsuya/f1faf5753b6eddf9cf4bc4dae276f181 to your computer and use it in GitHub Desktop.
Script for migrating Vue2 components to Compotision API

Script for migrating Vue2 components to CompositionAPI

https://github.com/thinreports/thinreports-section-editor の Vue2 形式のコンポーネントを CompositionAPI に一括で変換するための Ruby スクリプト。

⚠️Warning

あくまで https://github.com/thinreports/thinreports-section-editor のコンポーネントの移行のための使い捨てのスクリプト。 元のコードのインデント規則やコードスタイルなど、一定の前提の下で動作するものであり、汎用性は一切考慮していない。

Prerequisites

Ruby 3.1 (たぶん2.7や3.0でも動くと思う)

Usage

$ ruby migrate-to-composition-api.rb src/components/Component.vue
$ ruby migrate-to-composition-api.rb "src/components/**/*.vue"

Example

Not Supported

対応していないと わかっているもの

  • createdmountedwatch などのライフサイクルフック
  • $refs$forceUpdate などの $~ 系の Vue 関数

Note

propssetup 内ではリアクティブではないため、setup で参照している props がある場合は、setup の直後に

const { foo, bar } = toRefs(props)

が自動的に挿入される。

require 'pathname'
glob_pattern = ARGV.first
def migrate(target_path)
target = File.read(target_path)
target_template = target.match(%r|<template.+/template>|m).to_s
target_script = target.match(%r|<script.+/script>|m).to_s
return if target_script.empty?
data = target_script.match(/data \(\):?.+?\n }/m)
data_defs = []
if data
data_defs = data.to_s
.gsub("\n", '')
.match(/return ?{\s*(.+?)\s*?}/)[1]
.split(',')
.map { |d| d.split(':').map(&:strip) }
end
data_types = target_script.match(/type Data = {.+?\n};/m).to_s
data_types = data_types.scan(/^ (\w+?): (.+?);$/).to_h
props = target_script.match(/props: {.+?\n }/m)
props_names = props ? props.to_s.scan(/ (\w+): {/).flatten : []
referenced_prop_names = props_names.select { |pn| target_script.scan(/this\.#{pn}(?!\w)/).size > 0 }
computed = target_script.match(/computed: {.+?\n }/m)
computed_defs = []
if computed
cnames = computed.to_s.scan(/^ (\w+):? ?\(.*?\)/).flatten
computed_defs = cnames.map do |name|
oneliner = computed.to_s.match?(/#{name}: \(.*?\)/)
if oneliner
{
name: name,
oneliner: true,
code: computed.to_s.match(/^ #{name}: \(.*?\).+$/).to_s
}
else
{
name: name,
oneliner: false,
code: computed.to_s.match(/ #{name} \(.*?\).+?\n },?\n/m).to_s
}
end
end
end
meths = target_script.match(/methods: {.+?\n }/m)
meths_defs = []
if meths
mnames = meths.to_s.scan(/^ (?:async )?(\w+):? ?\(.*?\)/).flatten
meths_defs = mnames.map do |name|
oneliner = meths.to_s.match?(/#{name}: \(.*?\)/)
if oneliner
{
name: name,
oneliner: true,
code:meths.to_s.match(/^ (async )?#{name}: \(.*?\).+$/).to_s
}
else
{
name: name,
oneliner: false,
code: meths.to_s.match(/ (async )?#{name} \(.*?\).+?\n },?\n/m).to_s
}
end
end
end
# build setup
setup_args = [].tap do |args|
args << (referenced_prop_names.empty? ? '_' : 'props')
args << '{ emit }' if target_script.include?('this.$emit(')
end
setup_args.clear if setup_args == ['_']
setup = [].tap do |s|
s << " setup (#{setup_args.join(', ')}) {"
if referenced_prop_names.any?
s << "\n"
s << " const { #{referenced_prop_names.join(', ')} } = toRefs(props);\n"
end
returns = []
s << "\n" if data_defs.any?
data_defs.each do |name, default|
returns << name
type = data_types[name]
type = "<#{type}>" if type
s << " const #{name} = ref#{type}(#{default});\n"
end
s << "\n" if computed_defs.any?
computed_defs.each do |computed_def|
returns << computed_def[:name]
if computed_def[:oneliner]
computed_def[:code].sub!(/^ (\w+): ?\((.*?)\)(\: .+?)? => (.+?),?$/, " const \\1 = computed((\\2)\\3 => \\4);\n")
else
computed_def[:code].sub!(/^ (\w+) ?\((.*?)\)(\: .+?)? {\n/, " const \\1 = computed((\\2)\\3 => {\n")
computed_def[:code].sub!(/^ },?$/, ' });')
end
s << computed_def[:code]
end
s << "\n" if meths_defs.any?
meths_defs.each do |meths_def|
returns << meths_def[:name]
if meths_def[:oneliner]
meths_def[:code].sub!(/^ (async )?(\w+): ?\((.*?)\)(\: .+?)? => (.+?),?$/, " const \\2 = \\1(\\3)\\4 => \\5;\n")
else
meths_def[:code].sub!(/^ (async )?(\w+) ?\((.*?)\)(\: \w+)? {\n/, " const \\2 = \\1(\\3)\\4 => {\n")
meths_def[:code].sub!(/^ },?$/, ' };')
end
s << meths_def[:code]
end
returns.select! { |name| target_template.match?(/(?<!\w)#{name}(?!\w)/) }
if returns.any?
s << "\n"
s << " return {\n"
s << "#{returns.map.with_index { |n, i| " #{n}#{',' unless i == returns.size - 1}" }.join("\n")}\n"
s << " };\n"
end
s << ' }'
end
# setup
target_script.sub!(/^}\);/, "#{setup.join}\n});") if data_defs.any? || computed_defs.any? || meths_defs.any?
# convert code to new syntax and new package
packages = [].tap do |pk|
pk << 'computed' if computed_defs.any?
pk << 'defineComponent'
pk << 'nextTick' if target_script.include?('this.$nextTick(')
pk << 'ref' if data_defs.any?
pk << 'toRefs' if referenced_prop_names.any?
end
target_script.sub!(/(<script .+>\n)/, "\\1import { #{packages.join(', ')} } from '@vue/composition-api';\n#{ "\n" if target_script.count('^import ').zero? }")
target_script.sub!('Vue.extend', 'defineComponent')
referenced_prop_names.each do |name|
target_script.gsub!(/this\.#{name}(?!\w)/, "#{name}.value")
end
data_defs.each do |name, _|
target_script.gsub!(/this\.#{name}(?!\w)/, "#{name}.value")
end
props_names.each do |name|
target_script.gsub!(/this.#{name}(?!\w)/, name)
end
computed_defs.each do |computed_def|
name = computed_def[:name]
target_script.gsub!(/this.#{name}(?!\w)/, "#{name}.value")
end
meths_defs.each do |meths_def|
name = meths_def[:name]
target_script.gsub!(/this.#{name}(?!\w)/, name)
end
target_script.gsub!(/this.\$emit\(/, 'emit(')
target_script.gsub!(/this.\$nextTick\(/, 'nextTick(')
target_script.gsub!(/type: (.+?) as PropType<(.+)>/, 'type: \1 as () => \2')
target_script.gsub!(/type: Function as/, 'type: (Function as unknown) as')
# cleanup leagacy code
target_script.sub!(/^ name:.+\n/, '')
target_script.sub!(/^ data \(\):?.+? },?\n/m, '')
target_script.sub!(/^ computed: {.+?\n },?\n/m, '')
target_script.sub!(/^ methods: {.+?\n },?\n/m, '')
target_script.sub!(/^import .+ from 'vue';\n/, '')
target_script.sub!(/^type Data = {.+?\n};\n\n?/m, '')
File.write(target_path, target.sub(%r|<script.+/script>|m, target_script))
end
Pathname.pwd.glob(glob_pattern) do |path|
print path.relative_path_from(Pathname.pwd).to_s
migrate(path.to_s)
print ' ...ok'
ensure
puts ''
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment