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

Commit 38f3313

Browse files
committed
feat: add directive support
1 parent 7e0e47e commit 38f3313

File tree

6 files changed

+135
-11
lines changed

6 files changed

+135
-11
lines changed

src/core/parseSFC.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
11
import { Parser as HTMLParser, ParserOptions as HTMLParserOptions } from 'htmlparser2'
22
import type { ParserOptions } from '@babel/parser'
3-
import { camelize, capitalize, isHTMLTag, isSVGTag, isVoidTag } from '@vue/shared'
3+
import { isHTMLTag, isSVGTag, isVoidTag } from '@vue/shared'
44
import { ParsedSFC, ScriptSetupTransformOptions, ScriptTagMeta } from '../types'
55
import { getIdentifierUsages } from './identifiers'
66
import { parse } from './babel'
7+
import { pascalize } from './utils'
78

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

12+
const BUILD_IN_DIRECTIVES = new Set([
13+
'if',
14+
'else',
15+
'else-if',
16+
'for',
17+
'once',
18+
'model',
19+
'on',
20+
'bind',
21+
'slot',
22+
'slot-scope',
23+
'key',
24+
'ref',
25+
'text',
26+
'html',
27+
'show',
28+
'pre',
29+
'cloak',
30+
// 'el',
31+
// 'ref',
32+
])
33+
1134
export function parseSFC(code: string, id?: string, options?: ScriptSetupTransformOptions): ParsedSFC {
1235
const components = new Set<string>()
36+
const directives = new Set<string>()
1337
const expressions = new Set<string>()
1438
const identifiers = new Set<string>()
1539

@@ -52,7 +76,7 @@ export function parseSFC(code: string, id?: string, options?: ScriptSetupTransfo
5276

5377
function handleTemplateContent(name: string, attributes: Record<string, string>) {
5478
if (!isHTMLTag(name) && !isSVGTag(name) && !isVoidTag(name))
55-
components.add(capitalize(camelize(name)))
79+
components.add(pascalize((name)))
5680

5781
Object.entries(attributes).forEach(([key, value]) => {
5882
if (!value)
@@ -64,6 +88,11 @@ export function parseSFC(code: string, id?: string, options?: ScriptSetupTransfo
6488
else
6589
expressions.add(`(${value})`)
6690
}
91+
if (key.startsWith('v-')) {
92+
const directiveName = key.slice('v-'.length)
93+
if (!BUILD_IN_DIRECTIVES.has(directiveName))
94+
directives.add(`v${pascalize(directiveName)}`)
95+
}
6796
if (key === 'ref')
6897
identifiers.add(value)
6998
})
@@ -183,6 +212,7 @@ export function parseSFC(code: string, id?: string, options?: ScriptSetupTransfo
183212
id,
184213
template: {
185214
components,
215+
directives,
186216
identifiers,
187217
},
188218
scriptSetup,

src/core/transformScriptSetup.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { camelize, capitalize } from '@vue/shared'
21
import type { Node, ObjectExpression, Statement } from '@babel/types'
32
import generate from '@babel/generator'
43
import { partition } from '@antfu/utils'
54
import { ParsedSFC, ScriptSetupTransformOptions } from '../types'
65
import { applyMacros } from './macros'
76
import { getIdentifierDeclarations } from './identifiers'
87
import { t } from './babel'
8+
import { isNotNil, pascalize, uncapitalize } from './utils'
99

1010
function isAsyncImport(node: any) {
1111
if (node.type === 'VariableDeclaration') {
@@ -35,22 +35,24 @@ export function transformScriptSetup(sfc: ParsedSFC, options?: ScriptSetupTransf
3535
const declarations = new Set<string>()
3636
getIdentifierDeclarations(hoisted, declarations)
3737
getIdentifierDeclarations(setupBody, declarations)
38+
const declarationArray = Array.from(declarations).filter(isNotNil)
3839

3940
// filter out identifiers that are used in `<template>`
40-
const returns: ObjectExpression['properties'] = Array.from(declarations)
41-
.filter(Boolean)
41+
const returns: ObjectExpression['properties'] = declarationArray
4242
.filter(i => template.identifiers.has(i))
4343
.map((i) => {
4444
const id = t.identifier(i)
4545
return t.objectProperty(id, id, false, true)
4646
})
4747

48-
const components = Array.from(declarations)
49-
.filter(Boolean)
50-
.filter(i => template.components.has(i)
51-
|| template.components.has(camelize(i))
52-
|| template.components.has(capitalize(camelize(i))),
53-
)
48+
const components = Array.from(template.components).map(component =>
49+
declarationArray.find(declare => declare === component)
50+
?? declarationArray.find(declare => pascalize(declare) === component),
51+
).filter(isNotNil)
52+
53+
const directives = Array.from(template.directives).map(directive =>
54+
declarationArray.find(declare => declare === directive),
55+
).filter(isNotNil)
5456

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

@@ -160,6 +162,41 @@ export function transformScriptSetup(sfc: ParsedSFC, options?: ScriptSetupTransf
160162
)
161163
}
162164

165+
// inject directives
166+
// `__sfc_main.directives = Object.assign({ ... }, __sfc_main.directives)`
167+
if (directives.length) {
168+
hasBody = true
169+
const directivesObject = t.objectExpression(
170+
directives.map((i) => {
171+
// fooBar
172+
const keyName = uncapitalize(i.slice(1))
173+
// vFooBar
174+
const valueName = i
175+
return t.objectProperty(
176+
t.identifier(keyName),
177+
t.identifier(valueName),
178+
false,
179+
false,
180+
)
181+
}),
182+
)
183+
184+
ast.body.push(
185+
t.expressionStatement(
186+
t.assignmentExpression('=',
187+
t.memberExpression(__sfc, t.identifier('directives')),
188+
t.callExpression(
189+
t.memberExpression(t.identifier('Object'), t.identifier('assign')),
190+
[
191+
directivesObject,
192+
t.memberExpression(__sfc, t.identifier('directives')),
193+
],
194+
),
195+
),
196+
) as any,
197+
)
198+
}
199+
163200
if (!hasBody && !options?.astTransforms) {
164201
return {
165202
ast: null,

src/core/utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { camelize, capitalize } from '@vue/shared'
2+
3+
export const pascalize = (str: string) => capitalize(camelize(str))
4+
5+
export const uncapitalize = (str: string) => (str[0]?.toLowerCase() ?? '') + str.slice(1)
6+
7+
export const isNotNil = <T>(value: T): value is NonNullable<T> => value != null

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface ParsedSFC {
1818
id?: string
1919
template: {
2020
components: Set<string>
21+
directives: Set<string>
2122
identifiers: Set<string>
2223
}
2324
scriptSetup: ScriptTagMeta

test/__snapshots__/transform.test.ts.snap

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,40 @@ export default __sfc_main;
180180
"
181181
`;
182182

183+
exports[`transform fixtures test/fixtures/ComponentsDirectives.vue 1`] = `
184+
"<template>
185+
<div>
186+
<FooView ref=\\"fooView\\" v-foo-bar=\\"a\\"></FooView>
187+
<router-view></router-view>
188+
</div>
189+
</template>
190+
191+
<script lang=\\"ts\\">
192+
import { ref } from '@vue/runtime-dom';
193+
import FooView from './FooView.vue';
194+
import { vFooBar } from './foo-bar-directive';
195+
const __sfc_main = {};
196+
197+
__sfc_main.setup = (__props, __ctx) => {
198+
const fooView = ref<null | InstanceType<typeof FooView>>(null);
199+
const a = ref(1);
200+
return {
201+
fooView,
202+
a
203+
};
204+
};
205+
206+
__sfc_main.components = Object.assign({
207+
FooView
208+
}, __sfc_main.components);
209+
__sfc_main.directives = Object.assign({
210+
fooBar: vFooBar
211+
}, __sfc_main.directives);
212+
export default __sfc_main;
213+
</script>
214+
"
215+
`;
216+
183217
exports[`transform fixtures test/fixtures/DynamicStyle.vue 1`] = `
184218
"<template>
185219
<div :style=\\"{ color, border: '1px' }\\" />
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<template>
2+
<div>
3+
<FooView ref="fooView" v-foo-bar="a"></FooView>
4+
<router-view></router-view>
5+
</div>
6+
</template>
7+
8+
<script setup lang="ts">
9+
import { ref } from '@vue/runtime-dom'
10+
import FooView from './FooView.vue'
11+
import { vFooBar } from './foo-bar-directive'
12+
13+
const fooView = ref<null | InstanceType<typeof FooView>>(null)
14+
const a = ref(1)
15+
</script>

0 commit comments

Comments
 (0)