Skip to content

fix: use destructure handler for v-for #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 36 additions & 28 deletions src/utils/template.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AttributeNode, DirectiveNode, ExpressionNode, ParentNode, RootNode, SourceLocation, TemplateChildNode, TextNode } from '@vue/compiler-dom'
import type { AttributeNode, DirectiveNode, ExpressionNode, ForParseResult, ParentNode, RootNode, SourceLocation, TemplateChildNode, TextNode } from '@vue/compiler-dom'

// copy from `@vue/compiler-dom`
enum NodeTypes {
Expand Down Expand Up @@ -40,6 +40,7 @@ enum NodeTypes {
interface ExpressionTrack {
type: NodeTypes
name?: string
forParseResult?: ForParseResult
}

interface Expression {
Expand Down Expand Up @@ -163,17 +164,18 @@ export async function transpileVueTemplate(
for (const item of expressions) {
item.replacement = transformMap.get(item) ?? item.src

const surrounding = getSurrounding(
// the source should only have one of the quotes
const sourceQuote = getSourceQuote(
content,
item.loc.start.offset - offset,
item.loc.end.offset - offset,
)
if (surrounding) {
const replace = surrounding.code === `"` ? `'` : `"`
if (sourceQuote !== null) {
const search = sourceQuote === `"` ? `'` : `"`
item.replacement = replaceQuote(
item.replacement,
surrounding.code,
replace,
search,
sourceQuote,
)
}
}
Expand Down Expand Up @@ -210,25 +212,15 @@ function replaceQuote(code: string, target: string, replace: string): string {
return res
}

function getSurrounding(code: string, start: number, end: number) {
const empty = new Set<string | undefined>([' ', '\n', '\r', '\t'])
let startIndex = start - 1
let endIndex = end

while (startIndex > 0 && empty.has(code.at(startIndex))) {
startIndex--
}

while (endIndex < code.length && empty.has(code.at(endIndex))) {
endIndex++
function getSourceQuote(code: string, start: number, end: number): string | null {
const source = code.slice(start, end)
const quotes = ['"', '\'']
for (const quote of quotes) {
if (source.includes(quote)) {
return quote
}
}

const prev = startIndex >= 0 ? code.at(startIndex) : ''
const next = endIndex < code.length ? code.at(endIndex) : ''

return prev && next && prev === next
? { code: prev, prevAt: startIndex, nextAt: endIndex }
: undefined
return null
}

interface SnippetHandler {
Expand All @@ -254,11 +246,26 @@ const defaultSnippetHandler: SnippetHandler = {
standalone: false,
}

const vSlotSnippetHandler: SnippetHandler = {
const destructureSnippetHandler: SnippetHandler = {
key: (node) => {
const key = `destructure$:${node.src}`
const lastTrack = node.track.at(-1)
const secondLastTrack = node.track.at(-2)

// v-slot:xxx="{ name }"
if (secondLastTrack?.type === NodeTypes.DIRECTIVE && secondLastTrack.name === 'slot') {
return `vSlot$:${node.src}`
return key
}

// v-for="({ name }, key, index) of items"
// ^this ^this ^this ^not this
if (
secondLastTrack?.type === NodeTypes.DIRECTIVE
&& secondLastTrack.name === 'for'
&& secondLastTrack?.forParseResult
&& lastTrack !== secondLastTrack.forParseResult.source
) {
return key
}
return null
},
Expand All @@ -274,7 +281,7 @@ const vSlotSnippetHandler: SnippetHandler = {
standalone: true,
}

const snippetHandlers = [vSlotSnippetHandler, defaultSnippetHandler]
const snippetHandlers = [destructureSnippetHandler, defaultSnippetHandler]
function getKey(expression: Expression) {
for (const handler of snippetHandlers) {
const key = handler.key(expression)
Expand Down Expand Up @@ -339,7 +346,8 @@ async function transformJsSnippets(expressions: Expression[], transform: (code:

// transform standalone snippets
await Promise.all(standalone.map(async ({ id, handler, nodes }) => {
const line = await transform(handler.prepare(nodes[0], id))
const prepared = handler.prepare(nodes[0], id)
const line = await transform(prepared)

const res = handler.parse(line.trim(), id)
if (!res) {
Expand Down
7 changes: 5 additions & 2 deletions test/template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import { transpileVueTemplate } from '../src/utils/template'

describe('transform typescript template', () => {
it('v-for', async () => {
expect(await fixture(`<div v-for="item as string in items as unknown[]" :key="item">{{ item }}</div>`))
expect(await fixture(`<div v-for="item in items as unknown[]" :key="item">{{ item }}</div>`))
.toEqual(`<div v-for="item in items" :key="item">{{ item }}</div>`)

expect(await fixture(`<div v-for="(item as string, index) in items as unknown[]" :key="item" :index>{{ item }}</div>`))
expect(await fixture(`<div v-for="(item, index) in items as unknown[]" :key="item" :index>{{ item }}</div>`))
.toEqual(`<div v-for="(item, index) in items" :key="item" :index>{{ item }}</div>`)

expect(await fixture(`<div v-for="(item, index) of items" />`))
.toEqual(`<div v-for="(item, index) of items" />`)

expect(await fixture(`<div v-for="({ name = 'Tony' }, index) of items" />`))
.toEqual(`<div v-for="({ name = 'Tony' }, index) of items" />`)
})

it('v-if', async () => {
Expand Down