版博士V2.0程序
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. const $ = require('../index');
  2. const code = `
  3. <template>
  4. <div>
  5. <p>迁移:{{ name }}</p>
  6. <p>Vue版本:{{ version }}</p>
  7. <div>
  8. <div>arr:</div>
  9. <ul>
  10. <li :key="num" v-for="num in arr" ref="arr">
  11. <a :key="num"></a>
  12. {{ num }}
  13. </li>
  14. </ul>
  15. </div>
  16. <div>
  17. <div>arr from ref:</div>
  18. <ul>
  19. <li :key="ele.innerText" v-for="ele in arrFromRef">
  20. {{ ele.innerText }}
  21. </li>
  22. </ul>
  23. </div>
  24. </div>
  25. </template>
  26. <script>
  27. var a = 1;
  28. function c( sd) {
  29. s
  30. }
  31. </script>
  32. <style>
  33. .class11{
  34. color: #333;
  35. }
  36. </style>
  37. `;
  38. test('replace vue', () => {
  39. const output = $(code, { parseOptions: { language: 'vue' } })
  40. .find('<template></template>')
  41. .replace(
  42. `<$_$ :key="num" $$$1>$$$2</$_$>`,
  43. `<$_$ key="num:" $$$1>$$$2</$_$>`
  44. )
  45. .root()
  46. .find('<script></script>')
  47. .replace(`var a = 1;`, `var aaaaaaa = 1;`)
  48. .root()
  49. .generate();
  50. const res = output.match('key="num:"') && output.match('aaaaaaa');
  51. expect(res).toBeTruthy();
  52. });
  53. const keyCodeDemo = `
  54. <template>
  55. <div>
  56. <p>迁移:按键修饰符</p>
  57. <p>迁移策略:
  58. 1.Vue3不再支持使用数字 (即键码) 作为 v-on 修饰符
  59. 2.不再支持 config.keyCodes</p>
  60. <div te s="" class="mt20 text-left">
  61. // te todo
  62. <div>space:<input type="text" te s="" @keyup.space="keys('space')" /></div>
  63. <div>space:<input type="text" @keyup.32="keys('keycode 32 space')" /> </div>
  64. <div>space:<input type="text" @keyup.customSpace="keys('keycode 32 space')" /> </div>
  65. </div>
  66. </div>
  67. </template>
  68. <script>
  69. import Vue from 'vue';
  70. Vue.config.keyCodes = {
  71. customSpace: 32,
  72. customDelete: 46
  73. };
  74. export default {
  75. name: '按键修饰符',
  76. props: {},
  77. data() {},
  78. methods: {
  79. keys(key) {
  80. alert('您按下的是' + key);
  81. },
  82. },
  83. };
  84. </script>
  85. `;
  86. // 全量的keyCode对照表,基于篇幅这里只列出3个
  87. // https://developer.mozilla.org/zh-CN/docs/Web/API/KeyboardEvent/keyCode
  88. const keyCodeMap = { 46: 'delete', 32: 'space', 112: 'f1' };
  89. test('replace vue', () => {
  90. const ast = $(keyCodeDemo, {
  91. parseOptions: { language: 'vue' }
  92. });
  93. const scriptAst = ast.find('<script></script>');
  94. // 匹配取出自定义的keyCode,Node数组
  95. const customKeyCodeList = scriptAst.find(`Vue.config.keyCodes = {$_$}`)
  96. .match[0];
  97. //加上自定义keyCode构造汇总所有的keyCodeMap,待会替换template内容的时候需要使用
  98. for (let i = 0; i < customKeyCodeList.length; i = i + 2) {
  99. Object.assign(keyCodeMap, {
  100. [customKeyCodeList[i].value]:
  101. keyCodeMap[customKeyCodeList[i + 1].value]
  102. });
  103. //结果:{46: 'delete',32: 'space',112: 'f1', customSpace: 'space', customDelete: 'delete'}
  104. }
  105. scriptAst
  106. .find(`Vue.config.keyCodes = $_$`)
  107. .remove()
  108. const template = ast.find('<template></template>')
  109. .find(['<$_$></$_$>', '<$_$ />'])
  110. .each((node) => {
  111. //如果节点含有属性,则遍历它的属性
  112. if (Array.isArray(node.attr('content.attributes'))) {
  113. node.attr('content.attributes').forEach((attr) => {
  114. //使用上文构造出来的汇总keyCodeMap,替换匹配到的属性名 如@keyup.32 -> @keyup.space
  115. for (let keyItem in keyCodeMap) {
  116. if (attr.key.content.endsWith(`.${keyItem}`)) {
  117. attr.key.content = attr.key.content.replace(
  118. `.${keyItem}`,
  119. `.${keyCodeMap[keyItem]}`
  120. );
  121. }
  122. }
  123. });
  124. }
  125. })
  126. .root()
  127. .generate();
  128. const res = template.match(/keyup.space\="keys\('keycode 32 space'\)"/g).length == 2 && !template.match(`Vue.config.keyCodes`);
  129. expect(res).toBeTruthy();
  130. });
  131. const asyncDemo = `
  132. <template>
  133. <ul>
  134. <li :key="num" v-for="num in arr" ref="arr">
  135. {{ num }}
  136. </li>
  137. </ul>
  138. </template>
  139. <script>
  140. import Vue from 'vue';
  141. export default {
  142. name: 'v-for 中的 Ref 数组',
  143. data() {
  144. return {
  145. arr: [1, 2, 3, 4, 5]
  146. };
  147. }
  148. };
  149. </script>
  150. `
  151. test('replace ref', () => {
  152. // 先处理template,针对带有v-for且ref属性的标签,把ref属性名改为:ref,属性值改为函数调用getRefSetter()
  153. let ast = $(asyncDemo, { parseOptions: { language: 'vue' } })
  154. let templateRes = ast
  155. .find('<template></template>')
  156. .replace(`<$_$ v-for="$_$1" ref="$_$2" $$$1>$$$2</$_$>`,
  157. `<$_$ v-for="$_$1" :ref="getRefSetter('$_$2')" $$$1>$$$2</$_$>`)
  158. // 处理script,在method里加入getRefSetter函数定义
  159. let scriptRes = ast
  160. .find('<script></script>')
  161. .replace(`export default {
  162. $$$1,
  163. methods: {
  164. $$$2
  165. }
  166. }`, `
  167. export default {
  168. $$$1,
  169. methods: {
  170. $$$2,
  171. getRefSetter(refKey) {
  172. return (ref) => {
  173. !this.$arrRefs && (this.$arrRefs = {});
  174. !this.$arrRefs[refKey] && (this.$arrRefs.arr = []);
  175. ref && this.$arrRefs[refKey].push(ref);
  176. };
  177. }
  178. }
  179. }`)
  180. // 判断如果原本没有methods属性,就连method一起插入
  181. if (!scriptRes.has(`methods: {}`)) {
  182. scriptRes.replace(`export default {
  183. $$$1
  184. }`, `export default {
  185. methods: {
  186. getRefSetter(refKey) {
  187. return (ref) => {
  188. !this.$arrRefs && (this.$arrRefs = {});
  189. !this.$arrRefs[refKey] && (this.$arrRefs.arr = []);
  190. ref && this.$arrRefs[refKey].push(ref);
  191. };
  192. }
  193. },
  194. $$$1
  195. }`)
  196. }
  197. // 在判断原来有没有beforeUpdate
  198. if (scriptRes.has('beforeUpdate')) {
  199. scriptRes.find(`beforeUpdate() {}`)
  200. .append('body', $(`this.$arrRefs && (this.$arrRefs.arr = []);`, { isProgram: false }).node)
  201. } else {
  202. scriptRes.find(`export default {}`)
  203. .attr('declaration.properties').push($(`beforeUpdate() {
  204. this.$arrRefs && (this.$arrRefs.arr = []);
  205. }`, { isProgram: false }).node)
  206. }
  207. /**
  208. * open-delay changed to show-after
  209. * close-delay changed to hide-after
  210. * hide-after changed to auto-close
  211. **/
  212. expect(scriptRes.generate()).toBeTruthy();
  213. });
  214. const popoverDemo = `
  215. <template>
  216. <el-popover
  217. open-delay="10"
  218. hide-after="500"
  219. title="标题"
  220. width="200"
  221. content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。"
  222. v-model="visible">
  223. <el-button slot="reference" @click="visible = !visible">手动激活</el-button>
  224. </el-popover>
  225. </template>
  226. `
  227. test('replace ref', () => {
  228. const res = $(popoverDemo, { parseOptions: { language: 'vue' }})
  229. .find('<template></template>')
  230. .replace(`<el-popover open-delay="$_$" $$$1>$$$2</el-popover>`, `<el-popover show-after="$_$" $$$1>$$$2</el-popover>`)
  231. .replace(`<el-popover close-delay="$_$" $$$1>$$$2</el-popover>`, `<el-popover hide-after="$_$" $$$1>$$$2</el-popover>`)
  232. .replace(`<el-popover hide-after="$_$" $$$1>$$$2</el-popover>`, `<el-popover auto-close="$_$" $$$1>$$$2</el-popover>`)
  233. .root()
  234. .generate()
  235. expect(res.match(`show-after`)).toBeTruthy();
  236. });
  237. const asyncDemo1 = `
  238. <template fu>
  239. <li v-for="num in a" ref="arr">
  240. {{ num }}
  241. </li>
  242. </template>
  243. `
  244. test('replace ref', () => {
  245. // 先处理template,针对带有v-for且ref属性的标签,把ref属性名改为:ref,属性值改为函数调用getRefSetter()
  246. function tesssst(c) {
  247. let ast = $(c, { parseOptions: { language: 'vue' } });
  248. let templateRes = ast.find('<template></template>')
  249. .replace(`<$_$ v-for="$_$1" ref="$_$2" $$$1>$$$2</$_$>`,
  250. `<$_$ v-for="$_$1" :ref="getRefSetter('$_$2')" $$$1>$$$2</$_$>`)
  251. .root()
  252. const tAttr = templateRes.attr('template.attrs');
  253. delete tAttr.functional
  254. templateRes = templateRes
  255. .generate(); // gennerate会返回完整的sfc
  256. console.log(templateRes)
  257. }
  258. tesssst(asyncDemo1)
  259. tesssst(asyncDemo1)
  260. tesssst(asyncDemo1)
  261. })
  262. test('import', () => {
  263. const res = $(`
  264. <template>
  265. <div>
  266. <p>迁移:{{ name }}</p>
  267. <p>Vue版本:{{ version }}</p>
  268. <AsyncComp msg="异步组件加载成功"/>
  269. <AsyncCompOption msg="异步组件Option加载成功"/>
  270. </div>
  271. </template>
  272. <script>
  273. import Vue from 'vue';
  274. /* () => import() 会识别出错 */
  275. const AsyncComp = () => import('./Async');
  276. const AsyncCompOption = () => ({
  277. component: import('./Async'),
  278. delay: 500
  279. })
  280. export default {
  281. name: 'async-components',
  282. props: {
  283. msg: String,
  284. },
  285. data() {
  286. return {
  287. name: '异步组件',
  288. version: Vue.version,
  289. };
  290. },
  291. components: {
  292. AsyncComp,
  293. AsyncCompOption
  294. },
  295. };
  296. </script>
  297. <!-- Add "scoped" attribute to limit CSS to this component only -->
  298. <style scoped>
  299. h1 {
  300. color: #64b587;
  301. }
  302. </style>
  303. `, { parseOptions: { language: 'vue' }})
  304. .find('<script></script>')
  305. .generate()
  306. expect(res.match(`AsyncCompOption`)).toBeTruthy();
  307. })
  308. test('import', () => {
  309. const res = $(`
  310. <template>
  311. <div>
  312. <p>迁移:{{ name }}</p>
  313. <p>Vue版本:{{ version }}</p>
  314. <p v-if="visible" v-highlight="color">hehe</p>
  315. <p>vmForDirective:{{vmForDirective}}</p>
  316. <button @click="changeColor">更换颜色</button>
  317. <button @click="changeVisible">加载/卸载</button>
  318. </div>
  319. </template>
  320. <script>
  321. import Vue from 'vue';
  322. /* 加上 directives 解析就有问题了 */
  323. /* 迁移指南: https://v3.cn.vuejs.org/guide/migration/custom-directives.html */
  324. export default {
  325. name: 'custom-directives',
  326. props: {
  327. msg: String,
  328. },
  329. data() {
  330. return {
  331. name: 'custom-directives',
  332. version: Vue.version,
  333. color: 'yellow',
  334. visible: true,
  335. vmForDirective: 'before'
  336. };
  337. },
  338. methods: {
  339. changeColor() {
  340. this.color = this.color === 'yellow' ? 'red' : 'yellow';
  341. },
  342. changeVisible() {
  343. this.visible = !this.visible;
  344. },
  345. },
  346. directives: {
  347. highlight: {
  348. bind(el, binding, vnode) {
  349. el.style.background = binding.value;
  350. const vm = vnode.context
  351. vm.vmForDirective = 'after'
  352. },
  353. inserted(el, binding) {
  354. el.parentNode.style.border = \`1px solid \${binding.value}\`;
  355. },
  356. // and update
  357. componentUpdated(el, binding) {
  358. el.style.background = binding.value;
  359. el.parentNode.style.border = \`1px solid \${binding.value}\`;
  360. },
  361. unbind(el, binding) {
  362. alert('Vue2-directive-highlight 已经卸载');
  363. },
  364. },
  365. },
  366. };
  367. </script>
  368. <!-- Add "scoped" attribute to limit CSS to this component only -->
  369. <style scoped>
  370. h1 {
  371. color: #64b587;
  372. }
  373. </style>
  374. <template>
  375. <div>
  376. <p>迁移:{{ name }}</p>
  377. <p>Vue版本:{{ version }}</p>
  378. <p v-if="visible" v-highlight="color">hehe</p>
  379. <p>vmForDirective:{{vmForDirective}}</p>
  380. <button @click="changeColor">更换颜色</button>
  381. <button @click="changeVisible">加载/卸载</button>
  382. </div>
  383. </template>
  384. <script>
  385. import Vue from 'vue';
  386. /* 加上 directives 解析就有问题了 */
  387. /* 迁移指南: https://v3.cn.vuejs.org/guide/migration/custom-directives.html */
  388. export default {
  389. name: 'custom-directives',
  390. props: {
  391. msg: String,
  392. },
  393. data() {
  394. return {
  395. name: 'custom-directives',
  396. version: Vue.version,
  397. color: 'yellow',
  398. visible: true,
  399. vmForDirective: 'before'
  400. };
  401. },
  402. methods: {
  403. changeColor() {
  404. this.color = this.color === 'yellow' ? 'red' : 'yellow';
  405. },
  406. changeVisible() {
  407. this.visible = !this.visible;
  408. },
  409. },
  410. directives: {
  411. highlight: {
  412. bind(el, binding, vnode) {
  413. el.style.background = binding.value;
  414. const vm = vnode.context
  415. vm.vmForDirective = 'after'
  416. },
  417. inserted(el, binding) {
  418. el.parentNode.style.border = \`1px solid \${binding.value}\`;
  419. },
  420. // and update
  421. componentUpdated(el, binding) {
  422. el.style.background = binding.value;
  423. el.parentNode.style.border = \`1px solid \${binding.value}\`;
  424. },
  425. unbind(el, binding) {
  426. alert('Vue2-directive-highlight 已经卸载');
  427. },
  428. },
  429. },
  430. };
  431. </script>
  432. <!-- Add "scoped" attribute to limit CSS to this component only -->
  433. <style scoped>
  434. h1 {
  435. color: #64b587;
  436. }
  437. </style>
  438. `, { parseOptions: { language: 'vue' }})
  439. .find('<script></script>')
  440. .generate()
  441. expect(res.match(`Vue2-directive-highlight`)).toBeTruthy();
  442. })
  443. test('test no template', () => {
  444. const res = $(`
  445. <script>
  446. export default {
  447. name: 'components',
  448. data() {
  449. return {
  450. name: '组件'
  451. };
  452. }
  453. };
  454. </script>
  455. `, { parseOptions: { language: 'vue' }})
  456. .generate()
  457. expect(res.match(`<script>`)).toBeTruthy();
  458. })
  459. test('test script setup', () => {
  460. const res = $(`
  461. <template>
  462. <Layout :key="updateKey">
  463. <div>{{ $t('测试') }}</div>
  464. <div>哈哈哈</div>
  465. </Layout>
  466. </template>
  467. <script setup>
  468. var a = 1;
  469. </script>
  470. <script>
  471. var a = 1;
  472. </script>
  473. <style>
  474. #app {
  475. color: var(--theme-font-color);
  476. }
  477. </style>
  478. `, { parseOptions: { language: 'vue' }})
  479. .find('<script setup></script>')
  480. .replace('var a = 1', 'var b = 1')
  481. .root()
  482. .find('<script></script>')
  483. .replace('var a = 1', 'var c = 1')
  484. .root()
  485. .generate()
  486. expect(res.match(`var b = 1`) && res.match(`var c = 1`)).toBeTruthy();
  487. })
  488. test('test tag', () => {
  489. const res = $(`
  490. <template>
  491. <Notice
  492. v-for="notice in notices"
  493. :prefix-cls="prefixCls"
  494. >
  495. </Notice>
  496. </template>
  497. `, { parseOptions: { language: 'vue' }})
  498. .find('<template></template>')
  499. .has('<$_$ v-for="$_$1" ref="$_$2" $$$1>$$$2</$_$>')
  500. expect(!res).toBeTruthy();
  501. })
  502. test('test tag attr buttons', () => {
  503. const res = $(`
  504. <template>
  505. <Layout :key="\`buttons\`+ buttons + a.buttons + a.buttons.b + a.b.buttons">
  506. <div v-if="buttons.length">{{ 测试buttons }}</div>
  507. <div v-for="(item, index) in buttons">{{ buttonsbuttons }}</div>
  508. <div>哈哈哈</div>
  509. </Layout>
  510. </template>
  511. <script>
  512. </script>
  513. `, { parseOptions: { language: 'vue' }})
  514. .find('<template></template>')
  515. .find('<$_$1 $_$2="$_$3"></$_$1>')
  516. .each(tag => {
  517. tag.match[3].forEach(m => {
  518. if (!m.node.content.match('buttons')) return; // 不包含buttons就return
  519. m.node.content = // 给属性值赋新值
  520. $(m.node.content).find('buttons') // 将属性值处理为js表达式
  521. .each(item => {
  522. if (item.parent().node.type == 'MemberExpression' && item.parent().node.property.name == 'buttons') {
  523. // 如果buttons是被调用方,不改变
  524. return;
  525. }
  526. // 改变buttons变量为abc
  527. item.attr('name', 'abc')
  528. })
  529. .root()
  530. .generate()
  531. })
  532. })
  533. .generate()
  534. expect(res.match(/abc/g).length == 3).toBeTruthy();
  535. })
  536. test('test tag', () => {
  537. const ast = $(`
  538. <template>
  539. <div class="a">1</div>
  540. <div class="a">2</div>
  541. </template>
  542. <script>
  543. function getAbsoluteUrl(url) {
  544. import.meta.env.VITE_APP_BASE_URL
  545. console.log(url)
  546. if (!url) return ''
  547. }
  548. </script>`, { parseOptions: { language: 'vue', sourceType: 'module' } })
  549. const script = ast.find('<script></script>')
  550. script.replace('const a = $_$', 'const a = 2').generate()
  551. const res = ast.generate()
  552. expect(res.match('import.meta.env.VITE_APP_BASE_URL')).toBeTruthy()
  553. })
  554. test('test vue parseoptions', () => {
  555. const ast = $(`
  556. <template>
  557. <div class="a">1</div>
  558. <div class="a">2</div>
  559. </template>
  560. <script>
  561. function getAbsoluteUrl(url) {
  562. import.meta.env.VITE_APP_BASE_URL
  563. console.log(url)
  564. if (!url) return ''
  565. }
  566. </script>`, { parseOptions: { language: 'vue', sourceType: 'module' } })
  567. const script = ast
  568. .find('<template></template>').root()
  569. .find('<script></script>')
  570. const res = script.generate()
  571. expect(res.match('import.meta.env.VITE_APP_BASE_URL')).toBeTruthy()
  572. })
  573. test('test vue parseoptions', () => {
  574. const ast = $(
  575. `<template>
  576. <div v-if="a && b > 1">
  577. foo
  578. </div>
  579. </template>`,
  580. { parseOptions: { language: 'vue' } }
  581. )
  582. const res = ast.generate()
  583. expect(res.match('&&')).toBeTruthy()
  584. })