reactivityTransform.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. import {
  2. Node,
  3. Identifier,
  4. BlockStatement,
  5. CallExpression,
  6. ObjectPattern,
  7. ArrayPattern,
  8. Program,
  9. VariableDeclarator,
  10. Expression,
  11. VariableDeclaration
  12. } from '@babel/types'
  13. import MagicString, { SourceMap } from 'magic-string'
  14. import { walk } from 'estree-walker'
  15. import {
  16. extractIdentifiers,
  17. isFunctionType,
  18. isInDestructureAssignment,
  19. isReferencedIdentifier,
  20. isStaticProperty,
  21. walkFunctionParams
  22. } from '@vue/compiler-core'
  23. import { parse, ParserPlugin } from '@babel/parser'
  24. import { hasOwn, isArray, isString, genPropsAccessExp } from '@vue/shared'
  25. const CONVERT_SYMBOL = '$'
  26. const ESCAPE_SYMBOL = '$$'
  27. const shorthands = ['ref', 'computed', 'shallowRef', 'toRef', 'customRef']
  28. const transformCheckRE = /[^\w]\$(?:\$|ref|computed|shallowRef)?\s*(\(|\<)/
  29. export function shouldTransform(src: string): boolean {
  30. return transformCheckRE.test(src)
  31. }
  32. type Scope = Record<string, boolean | 'prop'>
  33. export interface RefTransformOptions {
  34. filename?: string
  35. sourceMap?: boolean
  36. parserPlugins?: ParserPlugin[]
  37. importHelpersFrom?: string
  38. }
  39. export interface RefTransformResults {
  40. code: string
  41. map: SourceMap | null
  42. rootRefs: string[]
  43. importedHelpers: string[]
  44. }
  45. export function transform(
  46. src: string,
  47. {
  48. filename,
  49. sourceMap,
  50. parserPlugins,
  51. importHelpersFrom = 'vue'
  52. }: RefTransformOptions = {}
  53. ): RefTransformResults {
  54. const plugins: ParserPlugin[] = parserPlugins || []
  55. if (filename) {
  56. if (/\.tsx?$/.test(filename)) {
  57. plugins.push('typescript')
  58. }
  59. if (filename.endsWith('x')) {
  60. plugins.push('jsx')
  61. }
  62. }
  63. const ast = parse(src, {
  64. sourceType: 'module',
  65. plugins
  66. })
  67. const s = new MagicString(src)
  68. const res = transformAST(ast.program, s, 0)
  69. // inject helper imports
  70. if (res.importedHelpers.length) {
  71. s.prepend(
  72. `import { ${res.importedHelpers
  73. .map(h => `${h} as _${h}`)
  74. .join(', ')} } from '${importHelpersFrom}'\n`
  75. )
  76. }
  77. return {
  78. ...res,
  79. code: s.toString(),
  80. map: sourceMap
  81. ? s.generateMap({
  82. source: filename,
  83. hires: true,
  84. includeContent: true
  85. })
  86. : null
  87. }
  88. }
  89. export function transformAST(
  90. ast: Program,
  91. s: MagicString,
  92. offset = 0,
  93. knownRefs?: string[],
  94. knownProps?: Record<
  95. string, // public prop key
  96. {
  97. local: string // local identifier, may be different
  98. default?: any
  99. }
  100. >
  101. ): {
  102. rootRefs: string[]
  103. importedHelpers: string[]
  104. } {
  105. // TODO remove when out of experimental
  106. warnExperimental()
  107. let convertSymbol = CONVERT_SYMBOL
  108. let escapeSymbol = ESCAPE_SYMBOL
  109. // macro import handling
  110. for (const node of ast.body) {
  111. if (
  112. node.type === 'ImportDeclaration' &&
  113. node.source.value === 'vue/macros'
  114. ) {
  115. // remove macro imports
  116. s.remove(node.start! + offset, node.end! + offset)
  117. // check aliasing
  118. for (const specifier of node.specifiers) {
  119. if (specifier.type === 'ImportSpecifier') {
  120. const imported = (specifier.imported as Identifier).name
  121. const local = specifier.local.name
  122. if (local !== imported) {
  123. if (imported === ESCAPE_SYMBOL) {
  124. escapeSymbol = local
  125. } else if (imported === CONVERT_SYMBOL) {
  126. convertSymbol = local
  127. } else {
  128. error(
  129. `macro imports for ref-creating methods do not support aliasing.`,
  130. specifier
  131. )
  132. }
  133. }
  134. }
  135. }
  136. }
  137. }
  138. const importedHelpers = new Set<string>()
  139. const rootScope: Scope = {}
  140. const scopeStack: Scope[] = [rootScope]
  141. let currentScope: Scope = rootScope
  142. let escapeScope: CallExpression | undefined // inside $$()
  143. const excludedIds = new WeakSet<Identifier>()
  144. const parentStack: Node[] = []
  145. const propsLocalToPublicMap = Object.create(null)
  146. if (knownRefs) {
  147. for (const key of knownRefs) {
  148. rootScope[key] = true
  149. }
  150. }
  151. if (knownProps) {
  152. for (const key in knownProps) {
  153. const { local } = knownProps[key]
  154. rootScope[local] = 'prop'
  155. propsLocalToPublicMap[local] = key
  156. }
  157. }
  158. function isRefCreationCall(callee: string): string | false {
  159. if (callee === convertSymbol) {
  160. return convertSymbol
  161. }
  162. if (callee[0] === '$' && shorthands.includes(callee.slice(1))) {
  163. return callee
  164. }
  165. return false
  166. }
  167. function error(msg: string, node: Node) {
  168. const e = new Error(msg)
  169. ;(e as any).node = node
  170. throw e
  171. }
  172. function helper(msg: string) {
  173. importedHelpers.add(msg)
  174. return `_${msg}`
  175. }
  176. function registerBinding(id: Identifier, isRef = false) {
  177. excludedIds.add(id)
  178. if (currentScope) {
  179. currentScope[id.name] = isRef
  180. } else {
  181. error(
  182. 'registerBinding called without active scope, something is wrong.',
  183. id
  184. )
  185. }
  186. }
  187. const registerRefBinding = (id: Identifier) => registerBinding(id, true)
  188. let tempVarCount = 0
  189. function genTempVar() {
  190. return `__$temp_${++tempVarCount}`
  191. }
  192. function snip(node: Node) {
  193. return s.original.slice(node.start! + offset, node.end! + offset)
  194. }
  195. function walkScope(node: Program | BlockStatement, isRoot = false) {
  196. for (const stmt of node.body) {
  197. if (stmt.type === 'VariableDeclaration') {
  198. walkVariableDeclaration(stmt, isRoot)
  199. } else if (
  200. stmt.type === 'FunctionDeclaration' ||
  201. stmt.type === 'ClassDeclaration'
  202. ) {
  203. if (stmt.declare || !stmt.id) continue
  204. registerBinding(stmt.id)
  205. } else if (
  206. (stmt.type === 'ForOfStatement' || stmt.type === 'ForInStatement') &&
  207. stmt.left.type === 'VariableDeclaration'
  208. ) {
  209. walkVariableDeclaration(stmt.left)
  210. } else if (
  211. stmt.type === 'ExportNamedDeclaration' &&
  212. stmt.declaration &&
  213. stmt.declaration.type === 'VariableDeclaration'
  214. ) {
  215. walkVariableDeclaration(stmt.declaration, isRoot)
  216. } else if (
  217. stmt.type === 'LabeledStatement' &&
  218. stmt.body.type === 'VariableDeclaration'
  219. ) {
  220. walkVariableDeclaration(stmt.body, isRoot)
  221. }
  222. }
  223. }
  224. function walkVariableDeclaration(stmt: VariableDeclaration, isRoot = false) {
  225. if (stmt.declare) {
  226. return
  227. }
  228. for (const decl of stmt.declarations) {
  229. let refCall
  230. const isCall =
  231. decl.init &&
  232. decl.init.type === 'CallExpression' &&
  233. decl.init.callee.type === 'Identifier'
  234. if (
  235. isCall &&
  236. (refCall = isRefCreationCall((decl as any).init.callee.name))
  237. ) {
  238. processRefDeclaration(refCall, decl.id, decl.init as CallExpression)
  239. } else {
  240. const isProps =
  241. isRoot && isCall && (decl as any).init.callee.name === 'defineProps'
  242. for (const id of extractIdentifiers(decl.id)) {
  243. if (isProps) {
  244. // for defineProps destructure, only exclude them since they
  245. // are already passed in as knownProps
  246. excludedIds.add(id)
  247. } else {
  248. registerBinding(id)
  249. }
  250. }
  251. }
  252. }
  253. }
  254. function processRefDeclaration(
  255. method: string,
  256. id: VariableDeclarator['id'],
  257. call: CallExpression
  258. ) {
  259. excludedIds.add(call.callee as Identifier)
  260. if (method === convertSymbol) {
  261. // $
  262. // remove macro
  263. s.remove(call.callee.start! + offset, call.callee.end! + offset)
  264. if (id.type === 'Identifier') {
  265. // single variable
  266. registerRefBinding(id)
  267. } else if (id.type === 'ObjectPattern') {
  268. processRefObjectPattern(id, call)
  269. } else if (id.type === 'ArrayPattern') {
  270. processRefArrayPattern(id, call)
  271. }
  272. } else {
  273. // shorthands
  274. if (id.type === 'Identifier') {
  275. registerRefBinding(id)
  276. // replace call
  277. s.overwrite(
  278. call.start! + offset,
  279. call.start! + method.length + offset,
  280. helper(method.slice(1))
  281. )
  282. } else {
  283. error(`${method}() cannot be used with destructure patterns.`, call)
  284. }
  285. }
  286. }
  287. function processRefObjectPattern(
  288. pattern: ObjectPattern,
  289. call: CallExpression,
  290. tempVar?: string,
  291. path: PathSegment[] = []
  292. ) {
  293. if (!tempVar) {
  294. tempVar = genTempVar()
  295. // const { x } = $(useFoo()) --> const __$temp_1 = useFoo()
  296. s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
  297. }
  298. for (const p of pattern.properties) {
  299. let nameId: Identifier | undefined
  300. let key: Expression | string | undefined
  301. let defaultValue: Expression | undefined
  302. if (p.type === 'ObjectProperty') {
  303. if (p.key.start! === p.value.start!) {
  304. // shorthand { foo }
  305. nameId = p.key as Identifier
  306. if (p.value.type === 'Identifier') {
  307. // avoid shorthand value identifier from being processed
  308. excludedIds.add(p.value)
  309. } else if (
  310. p.value.type === 'AssignmentPattern' &&
  311. p.value.left.type === 'Identifier'
  312. ) {
  313. // { foo = 1 }
  314. excludedIds.add(p.value.left)
  315. defaultValue = p.value.right
  316. }
  317. } else {
  318. key = p.computed ? p.key : (p.key as Identifier).name
  319. if (p.value.type === 'Identifier') {
  320. // { foo: bar }
  321. nameId = p.value
  322. } else if (p.value.type === 'ObjectPattern') {
  323. processRefObjectPattern(p.value, call, tempVar, [...path, key])
  324. } else if (p.value.type === 'ArrayPattern') {
  325. processRefArrayPattern(p.value, call, tempVar, [...path, key])
  326. } else if (p.value.type === 'AssignmentPattern') {
  327. if (p.value.left.type === 'Identifier') {
  328. // { foo: bar = 1 }
  329. nameId = p.value.left
  330. defaultValue = p.value.right
  331. } else if (p.value.left.type === 'ObjectPattern') {
  332. processRefObjectPattern(p.value.left, call, tempVar, [
  333. ...path,
  334. [key, p.value.right]
  335. ])
  336. } else if (p.value.left.type === 'ArrayPattern') {
  337. processRefArrayPattern(p.value.left, call, tempVar, [
  338. ...path,
  339. [key, p.value.right]
  340. ])
  341. } else {
  342. // MemberExpression case is not possible here, ignore
  343. }
  344. }
  345. }
  346. } else {
  347. // rest element { ...foo }
  348. error(`reactivity destructure does not support rest elements.`, p)
  349. }
  350. if (nameId) {
  351. registerRefBinding(nameId)
  352. // inject toRef() after original replaced pattern
  353. const source = pathToString(tempVar, path)
  354. const keyStr = isString(key)
  355. ? `'${key}'`
  356. : key
  357. ? snip(key)
  358. : `'${nameId.name}'`
  359. const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
  360. s.appendLeft(
  361. call.end! + offset,
  362. `,\n ${nameId.name} = ${helper(
  363. 'toRef'
  364. )}(${source}, ${keyStr}${defaultStr})`
  365. )
  366. }
  367. }
  368. }
  369. function processRefArrayPattern(
  370. pattern: ArrayPattern,
  371. call: CallExpression,
  372. tempVar?: string,
  373. path: PathSegment[] = []
  374. ) {
  375. if (!tempVar) {
  376. // const [x] = $(useFoo()) --> const __$temp_1 = useFoo()
  377. tempVar = genTempVar()
  378. s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
  379. }
  380. for (let i = 0; i < pattern.elements.length; i++) {
  381. const e = pattern.elements[i]
  382. if (!e) continue
  383. let nameId: Identifier | undefined
  384. let defaultValue: Expression | undefined
  385. if (e.type === 'Identifier') {
  386. // [a] --> [__a]
  387. nameId = e
  388. } else if (e.type === 'AssignmentPattern') {
  389. // [a = 1]
  390. nameId = e.left as Identifier
  391. defaultValue = e.right
  392. } else if (e.type === 'RestElement') {
  393. // [...a]
  394. error(`reactivity destructure does not support rest elements.`, e)
  395. } else if (e.type === 'ObjectPattern') {
  396. processRefObjectPattern(e, call, tempVar, [...path, i])
  397. } else if (e.type === 'ArrayPattern') {
  398. processRefArrayPattern(e, call, tempVar, [...path, i])
  399. }
  400. if (nameId) {
  401. registerRefBinding(nameId)
  402. // inject toRef() after original replaced pattern
  403. const source = pathToString(tempVar, path)
  404. const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
  405. s.appendLeft(
  406. call.end! + offset,
  407. `,\n ${nameId.name} = ${helper(
  408. 'toRef'
  409. )}(${source}, ${i}${defaultStr})`
  410. )
  411. }
  412. }
  413. }
  414. type PathSegmentAtom = Expression | string | number
  415. type PathSegment =
  416. | PathSegmentAtom
  417. | [PathSegmentAtom, Expression /* default value */]
  418. function pathToString(source: string, path: PathSegment[]): string {
  419. if (path.length) {
  420. for (const seg of path) {
  421. if (isArray(seg)) {
  422. source = `(${source}${segToString(seg[0])} || ${snip(seg[1])})`
  423. } else {
  424. source += segToString(seg)
  425. }
  426. }
  427. }
  428. return source
  429. }
  430. function segToString(seg: PathSegmentAtom): string {
  431. if (typeof seg === 'number') {
  432. return `[${seg}]`
  433. } else if (typeof seg === 'string') {
  434. return `.${seg}`
  435. } else {
  436. return snip(seg)
  437. }
  438. }
  439. function rewriteId(
  440. scope: Scope,
  441. id: Identifier,
  442. parent: Node,
  443. parentStack: Node[]
  444. ): boolean {
  445. if (hasOwn(scope, id.name)) {
  446. const bindingType = scope[id.name]
  447. if (bindingType) {
  448. const isProp = bindingType === 'prop'
  449. if (isStaticProperty(parent) && parent.shorthand) {
  450. // let binding used in a property shorthand
  451. // skip for destructure patterns
  452. if (
  453. !(parent as any).inPattern ||
  454. isInDestructureAssignment(parent, parentStack)
  455. ) {
  456. if (isProp) {
  457. if (escapeScope) {
  458. // prop binding in $$()
  459. // { prop } -> { prop: __props_prop }
  460. registerEscapedPropBinding(id)
  461. s.appendLeft(
  462. id.end! + offset,
  463. `: __props_${propsLocalToPublicMap[id.name]}`
  464. )
  465. } else {
  466. // { prop } -> { prop: __props.prop }
  467. s.appendLeft(
  468. id.end! + offset,
  469. `: ${genPropsAccessExp(propsLocalToPublicMap[id.name])}`
  470. )
  471. }
  472. } else {
  473. // { foo } -> { foo: foo.value }
  474. s.appendLeft(id.end! + offset, `: ${id.name}.value`)
  475. }
  476. }
  477. } else {
  478. if (isProp) {
  479. if (escapeScope) {
  480. // x --> __props_x
  481. registerEscapedPropBinding(id)
  482. s.overwrite(
  483. id.start! + offset,
  484. id.end! + offset,
  485. `__props_${propsLocalToPublicMap[id.name]}`
  486. )
  487. } else {
  488. // x --> __props.x
  489. s.overwrite(
  490. id.start! + offset,
  491. id.end! + offset,
  492. genPropsAccessExp(propsLocalToPublicMap[id.name])
  493. )
  494. }
  495. } else {
  496. // x --> x.value
  497. s.appendLeft(id.end! + offset, '.value')
  498. }
  499. }
  500. }
  501. return true
  502. }
  503. return false
  504. }
  505. const propBindingRefs: Record<string, true> = {}
  506. function registerEscapedPropBinding(id: Identifier) {
  507. if (!propBindingRefs.hasOwnProperty(id.name)) {
  508. propBindingRefs[id.name] = true
  509. const publicKey = propsLocalToPublicMap[id.name]
  510. s.prependRight(
  511. offset,
  512. `const __props_${publicKey} = ${helper(
  513. `toRef`
  514. )}(__props, '${publicKey}')\n`
  515. )
  516. }
  517. }
  518. // check root scope first
  519. walkScope(ast, true)
  520. ;(walk as any)(ast, {
  521. enter(node: Node, parent?: Node) {
  522. parent && parentStack.push(parent)
  523. // function scopes
  524. if (isFunctionType(node)) {
  525. scopeStack.push((currentScope = {}))
  526. walkFunctionParams(node, registerBinding)
  527. if (node.body.type === 'BlockStatement') {
  528. walkScope(node.body)
  529. }
  530. return
  531. }
  532. // catch param
  533. if (node.type === 'CatchClause') {
  534. scopeStack.push((currentScope = {}))
  535. if (node.param && node.param.type === 'Identifier') {
  536. registerBinding(node.param)
  537. }
  538. walkScope(node.body)
  539. return
  540. }
  541. // non-function block scopes
  542. if (node.type === 'BlockStatement' && !isFunctionType(parent!)) {
  543. scopeStack.push((currentScope = {}))
  544. walkScope(node)
  545. return
  546. }
  547. // skip type nodes
  548. if (
  549. parent &&
  550. parent.type.startsWith('TS') &&
  551. parent.type !== 'TSAsExpression' &&
  552. parent.type !== 'TSNonNullExpression' &&
  553. parent.type !== 'TSTypeAssertion'
  554. ) {
  555. return this.skip()
  556. }
  557. if (
  558. node.type === 'Identifier' &&
  559. // if inside $$(), skip unless this is a destructured prop binding
  560. !(escapeScope && rootScope[node.name] !== 'prop') &&
  561. isReferencedIdentifier(node, parent!, parentStack) &&
  562. !excludedIds.has(node)
  563. ) {
  564. // walk up the scope chain to check if id should be appended .value
  565. let i = scopeStack.length
  566. while (i--) {
  567. if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
  568. return
  569. }
  570. }
  571. }
  572. if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
  573. const callee = node.callee.name
  574. const refCall = isRefCreationCall(callee)
  575. if (refCall && (!parent || parent.type !== 'VariableDeclarator')) {
  576. return error(
  577. `${refCall} can only be used as the initializer of ` +
  578. `a variable declaration.`,
  579. node
  580. )
  581. }
  582. if (callee === escapeSymbol) {
  583. s.remove(node.callee.start! + offset, node.callee.end! + offset)
  584. escapeScope = node
  585. }
  586. // TODO remove when out of experimental
  587. if (callee === '$raw') {
  588. error(
  589. `$raw() has been replaced by $$(). ` +
  590. `See ${RFC_LINK} for latest updates.`,
  591. node
  592. )
  593. }
  594. if (callee === '$fromRef') {
  595. error(
  596. `$fromRef() has been replaced by $(). ` +
  597. `See ${RFC_LINK} for latest updates.`,
  598. node
  599. )
  600. }
  601. }
  602. },
  603. leave(node: Node, parent?: Node) {
  604. parent && parentStack.pop()
  605. if (
  606. (node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
  607. isFunctionType(node)
  608. ) {
  609. scopeStack.pop()
  610. currentScope = scopeStack[scopeStack.length - 1] || null
  611. }
  612. if (node === escapeScope) {
  613. escapeScope = undefined
  614. }
  615. }
  616. })
  617. return {
  618. rootRefs: Object.keys(rootScope).filter(key => rootScope[key] === true),
  619. importedHelpers: [...importedHelpers]
  620. }
  621. }
  622. const RFC_LINK = `https://github.com/vuejs/rfcs/discussions/369`
  623. const hasWarned: Record<string, boolean> = {}
  624. function warnExperimental() {
  625. // eslint-disable-next-line
  626. if (typeof window !== 'undefined') {
  627. return
  628. }
  629. warnOnce(
  630. `Reactivity transform is an experimental feature.\n` +
  631. `Experimental features may change behavior between patch versions.\n` +
  632. `It is recommended to pin your vue dependencies to exact versions to avoid breakage.\n` +
  633. `You can follow the proposal's status at ${RFC_LINK}.`
  634. )
  635. }
  636. function warnOnce(msg: string) {
  637. const isNodeProd =
  638. typeof process !== 'undefined' && process.env.NODE_ENV === 'production'
  639. if (!isNodeProd && !__TEST__ && !hasWarned[msg]) {
  640. hasWarned[msg] = true
  641. warn(msg)
  642. }
  643. }
  644. function warn(msg: string) {
  645. console.warn(
  646. `\x1b[1m\x1b[33m[@vue/reactivity-transform]\x1b[0m\x1b[33m ${msg}\x1b[0m\n`
  647. )
  648. }