Skip to content

Commit c9234da

Browse files
authored
fix: support v-slot transform (#12)
1 parent da253e8 commit c9234da

File tree

2 files changed

+115
-35
lines changed

2 files changed

+115
-35
lines changed

src/utils/template.ts

Lines changed: 109 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,40 @@ enum NodeTypes {
3737
JS_RETURN_STATEMENT,
3838
}
3939

40+
interface ExpressionTrack {
41+
type: NodeTypes
42+
name?: string
43+
}
44+
4045
interface Expression {
46+
track: ExpressionTrack[]
4147
loc: SourceLocation
4248
src: string
4349
replacement?: string
4450
}
4551

52+
type VueTemplateNode =
53+
| ParentNode
54+
| ExpressionNode
55+
| TemplateChildNode
56+
| AttributeNode
57+
| DirectiveNode
58+
4659
function handleNode(
47-
node:
48-
| ParentNode
49-
| ExpressionNode
50-
| TemplateChildNode
51-
| AttributeNode
52-
| DirectiveNode
53-
| undefined,
60+
node: VueTemplateNode | undefined,
5461
addExpression: (...expressions: Expression[]) => void,
62+
track: ExpressionTrack[],
5563
) {
5664
if (!node) {
5765
return
5866
}
5967

60-
const search = (node?: ExpressionNode | TemplateChildNode | AttributeNode | DirectiveNode | TextNode) => handleNode(node, addExpression)
68+
const currentTrack = [...track, node]
69+
70+
const search = (
71+
node?: ExpressionNode | TemplateChildNode | AttributeNode
72+
| DirectiveNode | TextNode,
73+
) => handleNode(node, addExpression, currentTrack)
6174

6275
switch (node.type) {
6376
case NodeTypes.ROOT: {
@@ -83,7 +96,7 @@ function handleNode(
8396
if (node.ast === null || node.ast === false) {
8497
return
8598
}
86-
addExpression({ loc: node.loc, src: node.content })
99+
addExpression({ loc: node.loc, src: node.content, track: currentTrack })
87100
return
88101
}
89102
case NodeTypes.INTERPOLATION: {
@@ -114,7 +127,7 @@ function handleNode(
114127
return
115128
}
116129

117-
addExpression({ loc: node.loc, src: node.loc.source })
130+
addExpression({ loc: node.loc, src: node.loc.source, track: currentTrack })
118131
return
119132
}
120133
// case NodeTypes.IF:
@@ -126,21 +139,26 @@ function handleNode(
126139
}
127140
}
128141

129-
export async function transpileVueTemplate(content: string, root: RootNode, offset = 0, transform: (code: string) => Promise<string>): Promise<string> {
142+
export async function transpileVueTemplate(
143+
content: string,
144+
root: RootNode,
145+
offset = 0,
146+
transform: (code: string) => Promise<string>,
147+
): Promise<string> {
130148
const { MagicString } = await import('vue/compiler-sfc')
131149
const expressions: Expression[] = []
132150

133-
handleNode(root, (...items) => expressions.push(...items))
151+
handleNode(root, (...items) => expressions.push(...items), [])
134152

135153
if (expressions.length === 0) {
136154
return content
137155
}
138156

139157
const s = new MagicString(content)
140158

141-
const transformMap = await transformJsSnippets(expressions.map(e => e.src), transform)
159+
const transformMap = await transformJsSnippets(expressions, transform)
142160
for (const item of expressions) {
143-
item.replacement = transformMap.get(item.src) ?? item.src
161+
item.replacement = transformMap.get(item) ?? item.src
144162

145163
const surrounding = getSurrounding(
146164
content,
@@ -210,42 +228,99 @@ function getSurrounding(code: string, start: number, end: number) {
210228
: undefined
211229
}
212230

213-
async function transformJsSnippets(codes: string[], transform: (code: string) => Promise<string>): Promise<Map<string, string>> {
214-
const keyMap = new Map<string, string>()
215-
const resMap = new Map<string, string>()
216-
const codeSet = new Set<string>()
231+
interface SnippetHandler {
232+
key: (node: Expression) => string | null
233+
prepare: (node: Expression, id: number) => string
234+
parse: (code: string, id: number) => string | undefined
235+
}
217236

218-
for (const code of codes) {
219-
if (codeSet.has(code)) {
220-
continue
237+
const defaultSnippetHandler: SnippetHandler = {
238+
key: node => `default$:${node.src}`,
239+
prepare: (node, id) => `wrapper_${id}(${node.src});`,
240+
parse: (code) => {
241+
const wrapperRegex = /^(wrapper_\d+)\(([\s\S]*?)\);$/
242+
243+
const [_, wrapperName, res] = code.match(wrapperRegex) ?? []
244+
if (!wrapperName || !res) {
245+
return undefined
221246
}
222247

223-
codeSet.add(code)
224-
keyMap.set(`wrapper_${keyMap.size}`, code)
248+
return res
249+
},
250+
}
251+
252+
const vSlotSnippetHandler: SnippetHandler = {
253+
key: (node) => {
254+
const secondLastTrack = node.track.at(-2)
255+
if (secondLastTrack?.type === NodeTypes.DIRECTIVE && secondLastTrack.name === 'slot') {
256+
return `vSlot$:${node.src}`
257+
}
258+
return null
259+
},
260+
prepare: (node, id) => `const ${node.src} = wrapper_${id}();`,
261+
parse: (code) => {
262+
const regex = /^(const\s+)(\w+)\s*=\s*wrapper_\d+\(\);$/
263+
const [_, res] = code.match(regex) ?? []
264+
if (!res) {
265+
return undefined
266+
}
267+
return res
268+
},
269+
}
270+
271+
async function transformJsSnippets(codes: Expression[], transform: (code: string) => Promise<string>): Promise<WeakMap<Expression, string>> {
272+
const keyMap = new WeakMap<Expression, string>()
273+
const transformMap = new Map<string, { id: number, node: Expression, handler: SnippetHandler }>()
274+
const snippetHandlers = [vSlotSnippetHandler, defaultSnippetHandler]
275+
let id = 0
276+
for (const code of codes) {
277+
for (const handler of snippetHandlers) {
278+
const key = handler.key(code)
279+
if (!key) {
280+
continue
281+
}
282+
283+
keyMap.set(code, key)
284+
if (transformMap.has(key)) {
285+
break
286+
}
287+
288+
transformMap.set(key, { id, node: code, handler })
289+
id += 1
290+
break
291+
}
225292
}
226293

294+
const batchOrder = Array.from(transformMap.entries())
295+
227296
// transform all snippets in a single file
228297
const batchInputSplitter = `\nsplitter(${Math.random()});\n`
229-
const batchInput = Array.from(keyMap.entries()).map(([wrapperName, raw]) => `${wrapperName}(${raw});`).join(batchInputSplitter)
298+
const batchInput = batchOrder
299+
.map(([_, { node, handler }]) => handler.prepare(node, id))
300+
.join(batchInputSplitter)
230301

231302
try {
232303
const batchOutput = await transform(batchInput)
304+
const lines = batchOutput.split(batchInputSplitter).map(l => l.trim()).filter(l => !!l)
233305

234-
const lines = batchOutput.trim().split(batchInputSplitter)
235-
const wrapperRegex = /^(wrapper_\d+)\(([\s\S]*?)\);$/
236-
for (const line of lines) {
237-
const [_, wrapperName, res] = line.match(wrapperRegex) ?? []
238-
if (!wrapperName || !res) {
306+
if (lines.length !== batchOrder.length) {
307+
throw new Error('[vue-sfc-transform] Syntax Error')
308+
}
309+
310+
const resultMap = new Map<Expression, string>()
311+
for (let i = 0; i < batchOrder.length; i++) {
312+
const line = lines[i]!
313+
const [_, { id, handler, node }] = batchOrder[i]!
314+
315+
const res = handler.parse(line, id)
316+
if (!res) {
239317
continue
240318
}
241319

242-
const raw = keyMap.get(wrapperName)
243-
if (raw) {
244-
resMap.set(raw, res)
245-
}
320+
resultMap.set(node, res)
246321
}
247322

248-
return resMap
323+
return resultMap
249324
}
250325
catch (error) {
251326
throw new Error('[vue-sfc-transform] Error parsing TypeScript expression in template', { cause: error })

test/template.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,19 @@ describe('transform typescript template', () => {
5454
`)
5555
})
5656

57+
it('v-slot', async () => {
58+
expect(await fixture(`<Comp><template #header="{ name = 'hi' }">{{ name!.toString() }}</template></Comp>`))
59+
.toMatchInlineSnapshot(`"<Comp><template #header="{ name = 'hi' }">{{ name.toString() }}</template></Comp>"`)
60+
})
61+
5762
it('destructuring', async () => {
5863
expect(
5964
await fixture(`<MyComponent v-slot="{ active, ...slotProps }">{{ active }}</MyComponent>`),
6065
).toEqual(`<MyComponent v-slot="{ active, ...slotProps }">{{ active }}</MyComponent>`)
6166

6267
expect(
6368
await fixture(
64-
`<MyComponent v-slot="{ remaining, duration } as { remaining: number, duration: number }">{{ remaining }}</MyComponent>`,
69+
`<MyComponent v-slot="{ remaining, duration }">{{ remaining }}</MyComponent>`,
6570
),
6671
).toMatchInlineSnapshot(`"<MyComponent v-slot="{ remaining, duration }">{{ remaining }}</MyComponent>"`)
6772
})

0 commit comments

Comments
 (0)