Skip to content

Commit

Permalink
feat: add vScope
Browse files Browse the repository at this point in the history
  • Loading branch information
AliceLanniste authored and sxzz committed May 5, 2023
1 parent a18c557 commit ce5b8f8
Show file tree
Hide file tree
Showing 11 changed files with 474 additions and 0 deletions.
98 changes: 98 additions & 0 deletions packages/v-scope/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"name": "@vue-macros/v-scope",
"version": "0.3.4",
"packageManager": "pnpm@7.31.0",
"description": "v-scope feature from Vue Macros.",
"keywords": [
"vue-macros",
"macros",
"vue",
"sfc",
"setup",
"script-setup",
"v-scope",
"unplugin"
],
"license": "MIT",
"homepage": "https://github.com/sxzz/unplugin-vue-macros#readme",
"bugs": {
"url": "https://github.com/sxzz/unplugin-vue-macros/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sxzz/unplugin-vue-macros.git",
"directory": "packages/named-template"
},
"author": "三咲智子 <sxzz@sxzz.moe>",
"files": [
"dist"
],
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"dev": "./src/index.ts",
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./api": {
"dev": "./src/api.ts",
"types": "./dist/api.d.ts",
"require": "./dist/api.js",
"import": "./dist/api.mjs"
},
"./esbuild": {
"dev": "./src/esbuild.ts",
"types": "./dist/esbuild.d.ts",
"require": "./dist/esbuild.js",
"import": "./dist/esbuild.mjs"
},
"./rollup": {
"dev": "./src/rollup.ts",
"types": "./dist/rollup.d.ts",
"require": "./dist/rollup.js",
"import": "./dist/rollup.mjs"
},
"./vite": {
"dev": "./src/vite.ts",
"types": "./dist/vite.d.ts",
"require": "./dist/vite.js",
"import": "./dist/vite.mjs"
},
"./webpack": {
"dev": "./src/webpack.ts",
"types": "./dist/webpack.d.ts",
"require": "./dist/webpack.js",
"import": "./dist/webpack.mjs"
},
"./*": [
"./*",
"./*.d.ts"
]
},
"typesVersions": {
"<=4.9": {
"*": [
"./dist/*",
"./*"
]
}
},
"scripts": {
"build": "tsup && tsx ../../scripts/postbuild.mts",
"dev": "DEV=true tsup"
},
"dependencies": {
"@vue-macros/common": "workspace:~",
"@vue/compiler-dom": "^3.3.0-alpha.8",
"unplugin": "^1.3.1"
},
"devDependencies": {
"vue": "^3.3.0-alpha.8"
},
"engines": {
"node": ">=14.19.0"
}
}
1 change: 1 addition & 0 deletions packages/v-scope/src/core/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SCOPE_DIRECTIVE = '_directive_scope'
136 changes: 136 additions & 0 deletions packages/v-scope/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {
MagicString,
babelParse,
getLang,
getTransformResult,
} from '@vue-macros/common'
import {
type ArrayExpression,
type ArrowFunctionExpression,
type BlockStatement,
type CallExpression,
type Identifier,
type ImportDeclaration,
type ObjectExpression,
type ObjectMethod,
type ObjectProperty,
type ReturnStatement,
type SequenceExpression,
type VariableDeclaration,
} from '@babel/types'
import { SCOPE_DIRECTIVE } from './constant'

export function preTransform(code: string, id: string) {
const s = new MagicString(code)

return getTransformResult(s, id)
}

export function postTransform(code: string, id: string) {
if (!code.includes(SCOPE_DIRECTIVE)) {
return
}

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

const valueStmt = sfcMainStmts.declarations[0].init as ObjectExpression

const setupStmt = valueStmt.properties[1]

const retStmt = (setupStmt as ObjectMethod).body.body[1] as ReturnStatement
const arguStmt = retStmt.argument as ArrowFunctionExpression

const innerVar = (arguStmt.body as BlockStatement)
.body[0] as VariableDeclaration
const innerRet = (arguStmt.body as BlockStatement).body[1] as ReturnStatement

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')

// overwrite render function
const direStart = innerVar.start!
const direEnd = innerVar.end!
s.remove(direStart, direEnd)
parseReturn(innerRet.argument as CallExpression, s)

return getTransformResult(s, id)
}

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 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
}
82 changes: 82 additions & 0 deletions packages/v-scope/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { createUnplugin } from 'unplugin'
import { type BaseOptions, type MarkRequired } from '@vue-macros/common'
import {
REGEX_VUE_SFC,
createFilter,
detectVueVersion,
} from '@vue-macros/common'
import { postTransform, preTransform } from './core'

export type Options = BaseOptions

export type OptionsResolved = MarkRequired<Options, 'include' | 'version'>

function resolveOption(options: Options): OptionsResolved {
const version = options.version || detectVueVersion()
return {
include: [REGEX_VUE_SFC],
...options,
version,
}
}

const name = 'unplugin-vue-v-scope'

export const PrePlugin = createUnplugin<Options | undefined, false>(
(userOptions = {}) => {
const options = resolveOption(userOptions)
const filter = createFilter(options)

return {
name: `${name}-pre`,
enforce: 'pre',

transformInclude(id) {
return filter(id)
},

transform(code, id) {
return preTransform(code, id)
},
}
}
)

export const PostPlugin = createUnplugin<Options | undefined, false>(
(userOptions = {}) => {
const options = resolveOption(userOptions)
const filter = createFilter(options)

function transformInclude(id: string) {
return filter(id)
}

return {
name: `${name}-post`,
enforce: 'post',

transformInclude,
transform(code, id) {
return postTransform(code, id)
},

rollup: {
transform: {
order: 'post',
handler(code, id) {
if (!transformInclude(id)) return
return postTransform(code, id)
},
},
},
}
}
)

const plugin = createUnplugin<Options | undefined, true>(
(userOptions = {}, meta) => {
return [PrePlugin.raw(userOptions, meta), PostPlugin.raw(userOptions, meta)]
}
)

export default plugin
3 changes: 3 additions & 0 deletions packages/v-scope/src/rollup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import unplugin from '.'

export default unplugin.rollup
Loading

0 comments on commit ce5b8f8

Please sign in to comment.