Skip to content
This repository has been archived by the owner on Dec 25, 2024. It is now read-only.

Commit

Permalink
feat: add directive support (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoxiangmoe authored Nov 28, 2021
1 parent 7e0e47e commit 837e4a5
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 11 deletions.
36 changes: 34 additions & 2 deletions src/core/parseSFC.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,41 @@
import { Parser as HTMLParser, ParserOptions as HTMLParserOptions } from 'htmlparser2'
import type { ParserOptions } from '@babel/parser'
import { camelize, capitalize, isHTMLTag, isSVGTag, isVoidTag } from '@vue/shared'
import { camelize, isHTMLTag, isSVGTag, isVoidTag } from '@vue/shared'
import { ParsedSFC, ScriptSetupTransformOptions, ScriptTagMeta } from '../types'
import { getIdentifierUsages } from './identifiers'
import { parse } from './babel'
import { pascalize } from './utils'

const multilineCommentsRE = /\/\*\s(.|[\r\n])*?\*\//gm
const singlelineCommentsRE = /\/\/\s.*/g

const BUILD_IN_DIRECTIVES = new Set([
'if',
'else',
'else-if',
'for',
'once',
'model',
'on',
'bind',
'slot',
'slot-scope',
'key',
'ref',
'text',
'html',
'show',
'pre',
'cloak',
// 'el',
// 'ref',
])

export function parseSFC(code: string, id?: string, options?: ScriptSetupTransformOptions): ParsedSFC {
/** foo-bar -> FooBar */
const components = new Set<string>()
/** v-foo-bar -> fooBar */
const directives = new Set<string>()
const expressions = new Set<string>()
const identifiers = new Set<string>()

Expand Down Expand Up @@ -52,7 +78,7 @@ export function parseSFC(code: string, id?: string, options?: ScriptSetupTransfo

function handleTemplateContent(name: string, attributes: Record<string, string>) {
if (!isHTMLTag(name) && !isSVGTag(name) && !isVoidTag(name))
components.add(capitalize(camelize(name)))
components.add(pascalize((name)))

Object.entries(attributes).forEach(([key, value]) => {
if (!value)
Expand All @@ -64,6 +90,11 @@ export function parseSFC(code: string, id?: string, options?: ScriptSetupTransfo
else
expressions.add(`(${value})`)
}
if (key.startsWith('v-')) {
const directiveName = key.slice('v-'.length).split(':')[0].split('.')[0]
if (!BUILD_IN_DIRECTIVES.has(directiveName))
directives.add(camelize(directiveName))
}
if (key === 'ref')
identifiers.add(value)
})
Expand Down Expand Up @@ -183,6 +214,7 @@ export function parseSFC(code: string, id?: string, options?: ScriptSetupTransfo
id,
template: {
components,
directives,
identifiers,
},
scriptSetup,
Expand Down
55 changes: 46 additions & 9 deletions src/core/transformScriptSetup.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { camelize, capitalize } from '@vue/shared'
import { capitalize } from '@vue/shared'
import type { Node, ObjectExpression, Statement } from '@babel/types'
import generate from '@babel/generator'
import { partition } from '@antfu/utils'
import { ParsedSFC, ScriptSetupTransformOptions } from '../types'
import { applyMacros } from './macros'
import { getIdentifierDeclarations } from './identifiers'
import { t } from './babel'
import { isNotNil, pascalize } from './utils'

function isAsyncImport(node: any) {
if (node.type === 'VariableDeclaration') {
Expand Down Expand Up @@ -35,22 +36,29 @@ export function transformScriptSetup(sfc: ParsedSFC, options?: ScriptSetupTransf
const declarations = new Set<string>()
getIdentifierDeclarations(hoisted, declarations)
getIdentifierDeclarations(setupBody, declarations)
const declarationArray = Array.from(declarations).filter(isNotNil)

// filter out identifiers that are used in `<template>`
const returns: ObjectExpression['properties'] = Array.from(declarations)
.filter(Boolean)
const returns: ObjectExpression['properties'] = declarationArray
.filter(i => template.identifiers.has(i))
.map((i) => {
const id = t.identifier(i)
return t.objectProperty(id, id, false, true)
})

const components = Array.from(declarations)
.filter(Boolean)
.filter(i => template.components.has(i)
|| template.components.has(camelize(i))
|| template.components.has(capitalize(camelize(i))),
)
const components = Array.from(template.components).map(component =>
declarationArray.find(declare => declare === component)
?? declarationArray.find(declare => pascalize(declare) === component),
).filter(isNotNil)

const directiveDeclaration = Array.from(template.directives).map((directive) => {
const identifier = declarationArray.find(declaration => declaration === `v${capitalize(directive)}`)
if (identifier === undefined)
return undefined

return { identifier, directive }
},
).filter(isNotNil)

// append `<script setup>` imports to `<script>`

Expand Down Expand Up @@ -160,6 +168,35 @@ export function transformScriptSetup(sfc: ParsedSFC, options?: ScriptSetupTransf
)
}

// inject directives
// `__sfc_main.directives = Object.assign({ ... }, __sfc_main.directives)`
if (directiveDeclaration.length) {
hasBody = true
const directivesObject = t.objectExpression(
directiveDeclaration.map(({ directive, identifier }) => (t.objectProperty(
t.identifier(directive),
t.identifier(identifier),
false,
false,
))),
)

ast.body.push(
t.expressionStatement(
t.assignmentExpression('=',
t.memberExpression(__sfc, t.identifier('directives')),
t.callExpression(
t.memberExpression(t.identifier('Object'), t.identifier('assign')),
[
directivesObject,
t.memberExpression(__sfc, t.identifier('directives')),
],
),
),
) as any,
)
}

if (!hasBody && !options?.astTransforms) {
return {
ast: null,
Expand Down
5 changes: 5 additions & 0 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { camelize, capitalize } from '@vue/shared'

export const pascalize = (str: string) => capitalize(camelize(str))

export const isNotNil = <T>(value: T): value is NonNullable<T> => value != null
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export interface ScriptTagMeta {
export interface ParsedSFC {
id?: string
template: {
/** foo-bar -> FooBar */
components: Set<string>
/** v-foo-bar -> fooBar */
directives: Set<string>
identifiers: Set<string>
}
scriptSetup: ScriptTagMeta
Expand Down
37 changes: 37 additions & 0 deletions test/__snapshots__/transform.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,43 @@ export default __sfc_main;
"
`;

exports[`transform fixtures test/fixtures/ComponentsDirectives.vue 1`] = `
"<template>
<div>
<FooView ref=\\"fooView\\" v-foo-bar=\\"a\\" v-demo:foo.a.b=\\"message\\"></FooView>
<router-view></router-view>
</div>
</template>
<script lang=\\"ts\\">
import { ref } from '@vue/runtime-dom';
import FooView from './FooView.vue';
import { vFooBar, vDemo } from './directive';
const __sfc_main = {};
__sfc_main.setup = (__props, __ctx) => {
const fooView = ref<null | InstanceType<typeof FooView>>(null);
const a = ref(1);
const message = ref('hello');
return {
fooView,
a,
message
};
};
__sfc_main.components = Object.assign({
FooView
}, __sfc_main.components);
__sfc_main.directives = Object.assign({
fooBar: vFooBar,
demo: vDemo
}, __sfc_main.directives);
export default __sfc_main;
</script>
"
`;
exports[`transform fixtures test/fixtures/DynamicStyle.vue 1`] = `
"<template>
<div :style=\\"{ color, border: '1px' }\\" />
Expand Down
16 changes: 16 additions & 0 deletions test/fixtures/ComponentsDirectives.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<template>
<div>
<FooView ref="fooView" v-foo-bar="a" v-demo:foo.a.b="message"></FooView>
<router-view></router-view>
</div>
</template>

<script setup lang="ts">
import { ref } from '@vue/runtime-dom'
import FooView from './FooView.vue'
import { vFooBar, vDemo } from './directive'
const fooView = ref<null | InstanceType<typeof FooView>>(null)
const a = ref(1)
const message = ref('hello')
</script>

0 comments on commit 837e4a5

Please sign in to comment.