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

Commit 837e4a5

Browse files
authored
feat: add directive support (#75)
1 parent 7e0e47e commit 837e4a5

File tree

6 files changed

+141
-11
lines changed

6 files changed

+141
-11
lines changed

src/core/parseSFC.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,41 @@
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 { camelize, 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 {
35+
/** foo-bar -> FooBar */
1236
const components = new Set<string>()
37+
/** v-foo-bar -> fooBar */
38+
const directives = new Set<string>()
1339
const expressions = new Set<string>()
1440
const identifiers = new Set<string>()
1541

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

5379
function handleTemplateContent(name: string, attributes: Record<string, string>) {
5480
if (!isHTMLTag(name) && !isSVGTag(name) && !isVoidTag(name))
55-
components.add(capitalize(camelize(name)))
81+
components.add(pascalize((name)))
5682

5783
Object.entries(attributes).forEach(([key, value]) => {
5884
if (!value)
@@ -64,6 +90,11 @@ export function parseSFC(code: string, id?: string, options?: ScriptSetupTransfo
6490
else
6591
expressions.add(`(${value})`)
6692
}
93+
if (key.startsWith('v-')) {
94+
const directiveName = key.slice('v-'.length).split(':')[0].split('.')[0]
95+
if (!BUILD_IN_DIRECTIVES.has(directiveName))
96+
directives.add(camelize(directiveName))
97+
}
6798
if (key === 'ref')
6899
identifiers.add(value)
69100
})
@@ -183,6 +214,7 @@ export function parseSFC(code: string, id?: string, options?: ScriptSetupTransfo
183214
id,
184215
template: {
185216
components,
217+
directives,
186218
identifiers,
187219
},
188220
scriptSetup,

src/core/transformScriptSetup.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { camelize, capitalize } from '@vue/shared'
1+
import { capitalize } from '@vue/shared'
22
import type { Node, ObjectExpression, Statement } from '@babel/types'
33
import generate from '@babel/generator'
44
import { partition } from '@antfu/utils'
55
import { ParsedSFC, ScriptSetupTransformOptions } from '../types'
66
import { applyMacros } from './macros'
77
import { getIdentifierDeclarations } from './identifiers'
88
import { t } from './babel'
9+
import { isNotNil, pascalize } from './utils'
910

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

3941
// filter out identifiers that are used in `<template>`
40-
const returns: ObjectExpression['properties'] = Array.from(declarations)
41-
.filter(Boolean)
42+
const returns: ObjectExpression['properties'] = declarationArray
4243
.filter(i => template.identifiers.has(i))
4344
.map((i) => {
4445
const id = t.identifier(i)
4546
return t.objectProperty(id, id, false, true)
4647
})
4748

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-
)
49+
const components = Array.from(template.components).map(component =>
50+
declarationArray.find(declare => declare === component)
51+
?? declarationArray.find(declare => pascalize(declare) === component),
52+
).filter(isNotNil)
53+
54+
const directiveDeclaration = Array.from(template.directives).map((directive) => {
55+
const identifier = declarationArray.find(declaration => declaration === `v${capitalize(directive)}`)
56+
if (identifier === undefined)
57+
return undefined
58+
59+
return { identifier, directive }
60+
},
61+
).filter(isNotNil)
5462

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

@@ -160,6 +168,35 @@ export function transformScriptSetup(sfc: ParsedSFC, options?: ScriptSetupTransf
160168
)
161169
}
162170

171+
// inject directives
172+
// `__sfc_main.directives = Object.assign({ ... }, __sfc_main.directives)`
173+
if (directiveDeclaration.length) {
174+
hasBody = true
175+
const directivesObject = t.objectExpression(
176+
directiveDeclaration.map(({ directive, identifier }) => (t.objectProperty(
177+
t.identifier(directive),
178+
t.identifier(identifier),
179+
false,
180+
false,
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { camelize, capitalize } from '@vue/shared'
2+
3+
export const pascalize = (str: string) => capitalize(camelize(str))
4+
5+
export const isNotNil = <T>(value: T): value is NonNullable<T> => value != null

src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ export interface ScriptTagMeta {
1717
export interface ParsedSFC {
1818
id?: string
1919
template: {
20+
/** foo-bar -> FooBar */
2021
components: Set<string>
22+
/** v-foo-bar -> fooBar */
23+
directives: Set<string>
2124
identifiers: Set<string>
2225
}
2326
scriptSetup: ScriptTagMeta

test/__snapshots__/transform.test.ts.snap

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,43 @@ 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\\" v-demo:foo.a.b=\\"message\\"></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, vDemo } from './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+
const message = ref('hello');
201+
return {
202+
fooView,
203+
a,
204+
message
205+
};
206+
};
207+
208+
__sfc_main.components = Object.assign({
209+
FooView
210+
}, __sfc_main.components);
211+
__sfc_main.directives = Object.assign({
212+
fooBar: vFooBar,
213+
demo: vDemo
214+
}, __sfc_main.directives);
215+
export default __sfc_main;
216+
</script>
217+
"
218+
`;
219+
183220
exports[`transform fixtures test/fixtures/DynamicStyle.vue 1`] = `
184221
"<template>
185222
<div :style=\\"{ color, border: '1px' }\\" />
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<template>
2+
<div>
3+
<FooView ref="fooView" v-foo-bar="a" v-demo:foo.a.b="message"></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, vDemo } from './directive'
12+
13+
const fooView = ref<null | InstanceType<typeof FooView>>(null)
14+
const a = ref(1)
15+
const message = ref('hello')
16+
</script>

0 commit comments

Comments
 (0)