Skip to content

Commit

Permalink
feat:support nested v-scope
Browse files Browse the repository at this point in the history
  • Loading branch information
AliceLanniste committed May 3, 2023
1 parent 1271a98 commit 57eb686
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 88 deletions.
158 changes: 71 additions & 87 deletions packages/v-scope/src/core/index.ts
Expand Up @@ -17,15 +17,9 @@ import {
type ReturnStatement,
type SequenceExpression,
type VariableDeclaration,
type VariableDeclarator,
} from '@babel/types'
import { SCOPE_DIRECTIVE } from './constant'

const SCOPE_REXP = /_directive_scope/g
enum matchedEnum {
SINGLE = 1,
MORE = 2,
}
export function preTransform(code: string, id: string) {
const s = new MagicString(code)

Expand All @@ -36,11 +30,10 @@ export function postTransform(code: string, id: string) {
if (!code.includes(SCOPE_DIRECTIVE)) {
return
}
let scopeMatch: matchedEnum = matchedEnum.SINGLE

const lang = getLang(id)
const program = babelParse(code, lang)
const s = new MagicString(code)

const importStatement = program.body[0] as ImportDeclaration

const sfcMainStmts = program.body[2] as VariableDeclaration
Expand All @@ -56,97 +49,88 @@ export function postTransform(code: string, id: string) {
.body[0] as VariableDeclaration
const innerRet = (arguStmt.body as BlockStatement).body[1] as ReturnStatement

const innerRetStr = code.slice(innerRet!.start!, innerRet!.end!)
const matchNum = matchedScopeKey(innerRetStr, SCOPE_REXP)
if (matchNum.length > 1) {
scopeMatch = matchedEnum.MORE
}
const safeSpecifier = ['toDisplayString', 'createTextVNode']
const specifiersFilter = importStatement.specifiers.filter((item) => {
return (
item.type === 'ImportSpecifier' &&
item.imported.type === 'Identifier' &&
!safeSpecifier.includes(item.imported.name)
)
})
const importStart = specifiersFilter[0].start
const importEnd = specifiersFilter[specifiersFilter.length - 1].end
s.remove(importStart!, importEnd!)

if (scopeMatch === matchedEnum.SINGLE) {
const safeSpecifier = ['toDisplayString', 'createTextVNode']
const specifiersFilter = importStatement.specifiers.filter((item) => {
return (
item.type === 'ImportSpecifier' &&
item.imported.type === 'Identifier' &&
!safeSpecifier.includes(item.imported.name)
)
})
const importStart = specifiersFilter[0].start
const importEnd = specifiersFilter[specifiersFilter.length - 1].end
s.remove(importStart!, importEnd!)

s.prependRight(importStart!, 'createElementVNode as _createElementVNode')
}
s.prependRight(importStart!, 'createElementVNode as _createElementVNode')

// overwrite render function
const direStart = innerVar.start!
const direEnd = innerVar.end!
s.remove(direStart, direEnd)
const argStmt = innerRet.argument as CallExpression
const arrayExp = (argStmt.arguments[1] as ArrayExpression)
.elements[0] as ArrayExpression
const isWithDirective =
argStmt.callee.type === 'Identifier' &&
argStmt.callee.name === '_withDirectives'

const isDirectiveScope =
arrayExp.elements[0]?.type === 'Identifier' &&
arrayExp.elements[0].name === '_directive_scope' &&
arrayExp.elements[1]?.type === 'ObjectExpression'
if (isWithDirective && isDirectiveScope) {
parseReturn(innerRet, s)
}
parseReturn(innerRet.argument as CallExpression, s)

return getTransformResult(s, id)
}

function parseReturn(stmt: ReturnStatement, s: MagicString) {
const argStmt = stmt.argument as CallExpression
const sequence = argStmt.arguments[0] as SequenceExpression
const targetSeq = sequence.expressions[1]
const targetSeqStart = targetSeq.start
const targetSeqEnd = targetSeq.end
const arrayExp = argStmt.arguments[1] as ArrayExpression
const arrStr = (arrayExp.elements[0] as ArrayExpression)
.elements[1] as ObjectExpression
const key = (arrStr.properties[0] as ObjectProperty).key as Identifier
const argStart = arrStr.start
const argEnd = arrStr.end
const argStr = s.slice(argStart!, argEnd!)
const ctxArg = `_ctx.${key.name}`

const targetSeqStr = s.slice(targetSeqStart!, targetSeqEnd!)
const changedTarget = targetSeqStr
.replace(ctxArg, key.name)
.replace('_createElementBlock', '_createElementVNode')
const changedStr = `${argStr
.replace(':', '=')
.replace('{', '(')
.replace('}', ')')}=>`

const changedAllStr = `(${changedStr} ${changedTarget})()`
s.remove(argStmt.start!, argStmt.end!)
s.prependRight(argStmt.start!, changedAllStr)
function parseReturn(argStmt: CallExpression, s: MagicString) {
if (detectReturnWithDir(argStmt)) {
const sequence = argStmt.arguments[0] as SequenceExpression
const targetSeq = sequence.expressions[1] as CallExpression

const targetSeqStart = targetSeq.start
const targetSeqEnd = targetSeq.end
const arrayExp = argStmt.arguments[1] as ArrayExpression
const arrStr = (arrayExp.elements[0] as ArrayExpression)
.elements[1] as ObjectExpression
const key = (arrStr.properties[0] as ObjectProperty).key as Identifier
const argStart = arrStr.start
const argEnd = arrStr.end
const nestedExp = targetSeq.arguments[2]
if (nestedExp.type === 'ArrayExpression' && nestedScope(nestedExp)) {
parseReturn(nestedExp.elements[0] as CallExpression, s)
}
const argStr = s.slice(argStart!, argEnd!)
const ctxArg = `_ctx.${key.name}`

const targetSeqStr = s.slice(targetSeqStart!, targetSeqEnd!)
const changedTarget = targetSeqStr
.replace(ctxArg, key.name)
.replace('_createElementBlock', '_createElementVNode')
const changedStr = `${argStr
.replace(':', '=')
.replace('{', '(')
.replace('}', ')')}=>`

const changedAllStr = `(${changedStr} ${changedTarget})()`

s.remove(argStmt.start!, argStmt.end!)
s.prependRight(argStmt.start!, changedAllStr)
}
}

// function detectVScope(scopeDecl: VariableDeclarator): Boolean {
// const callExp = scopeDecl.init
// if (!callExp) {
// return false
// }
// if (callExp.type !== 'CallExpression') {
// return false
// }
// const callee = callExp.callee
// const arg = callExp.arguments[0]
// const isDir =
// callee.type === 'Identifier' && callee.name === '_resolveDirective'
// const isScopeArg = arg.type === 'StringLiteral' && arg.value === 'scope'
// const isIdentifer =
// scopeDecl.id.type === 'Identifier' &&
// scopeDecl.id.name === '_directive_scope'
// return isIdentifer && isDir && isScopeArg
// }
function nestedScope(exp: ArrayExpression) {
const ele = exp.elements[0]
return ele && ele.type === 'CallExpression' && detectReturnWithDir(ele)
}

function matchedScopeKey(code: string, keyword: RegExp): RegExpMatchArray[] {
return [...code.matchAll(keyword)]
}

function detectReturnWithDir(exp: CallExpression) {
const isDir =
exp.callee.type === 'Identifier' && exp.callee.name === '_withDirectives'
const scopeArg = exp.arguments[1] as ArrayExpression
return isDir && iscorrectScopeArg(scopeArg)
}

function iscorrectScopeArg(arg: ArrayExpression) {
const argElements = arg.elements
const innerArgElements = (argElements[0] as ArrayExpression).elements
const isscopeId =
innerArgElements[0]!.type === 'Identifier' &&
innerArgElements[0]!.name === '_directive_scope'
const isScopeArg = innerArgElements[1]?.type === 'ObjectExpression'

return argElements.length === 1 && isscopeId && isScopeArg
}
28 changes: 28 additions & 0 deletions packages/v-scope/tests/__snapshots__/fixtures.test.ts.snap
Expand Up @@ -26,6 +26,34 @@ export { basic as default };
"
`;
exports[`fixtures > tests/fixtures/nested.vue 1`] = `
"import { ref, createElementVNode, createTextVNode, toDisplayString } from 'vue';
import _export_sfc from '/plugin-vue/export-helper';
const _sfc_main = {
__name: 'nested',
setup(__props) {
const msg = ref('Hello');
return (_ctx, _cache) => {
return (( a=1 )=> createElementVNode(\\"div\\", null, [
(( b=1 )=> createElementVNode(\\"span\\", null, [
createTextVNode(toDisplayString(a) + toDisplayString(b), 1 /* TEXT */)
]))()
]))()
}
}
};
var nested = /*#__PURE__*/_export_sfc(_sfc_main, [__FILE__]);
export { nested as default };
"
`;
exports[`fixtures > tests/fixtures/withoutVscope.vue 1`] = `
"import { ref, openBlock, createElementBlock, toDisplayString } from 'vue';
import _export_sfc from '/plugin-vue/export-helper';
Expand Down
2 changes: 1 addition & 1 deletion packages/v-scope/tests/fixtures.test.ts
Expand Up @@ -12,7 +12,7 @@ import VScope from '../src/rollup'

describe('fixtures', async () => {
await testFixtures(
'tests/fixtures/withoutVscope.vue',
'tests/fixtures/*.vue',
(args, id) =>
rollupBuild(id, [
VScope(),
Expand Down
12 changes: 12 additions & 0 deletions packages/v-scope/tests/fixtures/nested.vue
@@ -0,0 +1,12 @@

<script setup>
import { ref } from 'vue'
const msg = ref('Hello')
</script>

<template>
<div v-scope="{ a:1 }">
<span v-scope="{ b:1 }">{{ a }}{{ b }}</span>
</div>

</template>

0 comments on commit 57eb686

Please sign in to comment.