Skip to content
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

fix(nuxt): correctly move v-if from slot to wrapper component #26386

Merged
merged 12 commits into from
Mar 25, 2024
51 changes: 36 additions & 15 deletions packages/nuxt/src/components/islandsTransform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const HAS_SLOT_OR_CLIENT_RE = /(<slot[^>]*>)|(nuxt-client)/
const TEMPLATE_RE = /<template>([\s\S]*)<\/template>/
const NUXTCLIENT_ATTR_RE = /\s:?nuxt-client(="[^"]*")?/g
const IMPORT_CODE = '\nimport { vforToArray as __vforToArray } from \'#app/components/utils\'' + '\nimport NuxtTeleportIslandComponent from \'#app/components/nuxt-teleport-island-component\'' + '\nimport NuxtTeleportSsrSlot from \'#app/components/nuxt-teleport-island-slot\''
const EXTRACTED_ATTRS_RE = /v-(?:if|else-if|else)(="[^"]*")?/g

function wrapWithVForDiv (code: string, vfor: string): string {
return `<div v-for="${vfor}" style="display: contents;">${code}</div>`
Expand Down Expand Up @@ -79,29 +80,31 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
if (node.name === 'slot') {
const { attributes, children, loc } = node

// pass slot fallback to NuxtTeleportSsrSlot fallback
if (children.length) {
const attrString = Object.entries(attributes).map(([name, value]) => name ? `${name}="${value}" ` : value).join(' ')
const slice = code.slice(startingIndex + loc[0].end, startingIndex + loc[1].start).replaceAll(/:?key="[^"]"/g, '')
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[1].end, `<slot ${attrString} /><template #fallback>${attributes['v-for'] ? wrapWithVForDiv(slice, attributes['v-for']) : slice}</template>`)
}

const slotName = attributes.name ?? 'default'
let vfor: [string, string] | undefined
let vfor: string | undefined
if (attributes['v-for']) {
vfor = attributes['v-for'].split(' in ').map((v: string) => v.trim()) as [string, string]
vfor = attributes['v-for']
}
delete attributes['v-for']

if (attributes.name) { delete attributes.name }
if (attributes['v-bind']) {
attributes._bind = attributes['v-bind']
delete attributes['v-bind']
attributes._bind = extractAttributes(attributes, ['v-bind'])['v-bind']
}
const bindings = getPropsToString(attributes, vfor)

const teleportAttributes = extractAttributes(attributes, ['v-if', 'v-else-if', 'v-else'])
const bindings = getPropsToString(attributes, vfor?.split(' in ').map((v: string) => v.trim()) as [string, string])
// add the wrapper
s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportSsrSlot name="${slotName}" :props="${bindings}">`)
s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportSsrSlot${attributeToString(teleportAttributes)} name="${slotName}" :props="${bindings}">`)

if (children.length) {
// pass slot fallback to NuxtTeleportSsrSlot fallback
const attrString = attributeToString(attributes)
const slice = code.slice(startingIndex + loc[0].end, startingIndex + loc[1].start).replaceAll(/:?key="[^"]"/g, '')
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[1].end, `<slot${attrString.replaceAll(EXTRACTED_ATTRS_RE, '')}/><template #fallback>${vfor ? wrapWithVForDiv(slice, vfor) : slice}</template>`)
} else {
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, code.slice(startingIndex + loc[0].start, startingIndex + loc[0].end).replaceAll(EXTRACTED_ATTRS_RE, ''))
}

s.appendRight(startingIndex + loc[1].end, '</NuxtTeleportSsrSlot>')
} else if (options.selectiveClient && ('nuxt-client' in node.attributes || ':nuxt-client' in node.attributes)) {
hasNuxtClient = true
Expand Down Expand Up @@ -132,13 +135,31 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
}
})

/**
* extract attributes from a node
*/
function extractAttributes (attributes: Record<string, string>, names: string[]) {
const extracted:Record<string, string> = {}
for (const name of names) {
if (name in attributes) {
extracted[name] = attributes[name]
delete attributes[name]
}
}
return extracted
}

function attributeToString (attributes: Record<string, string>) {
return Object.entries(attributes).map(([name, value]) => value ? ` ${name}="${value}"` : ` ${name}`).join('')
}

function isBinding (attr: string): boolean {
return attr.startsWith(':')
}

function getPropsToString (bindings: Record<string, string>, vfor?: [string, string]): string {
if (Object.keys(bindings).length === 0) { return 'undefined' }
const content = Object.entries(bindings).filter(b => b[0] && b[0] !== '_bind').map(([name, value]) => isBinding(name) ? `${name.slice(1)}: ${value}` : `${name}: \`${value}\``).join(',')
const content = Object.entries(bindings).filter(b => b[0] && b[0] !== '_bind').map(([name, value]) => isBinding(name) ? `[\`${name.slice(1)}\`]: ${value}` : `[\`${name}\`]: \`${value}\``).join(',')
const data = bindings._bind ? `mergeProps(${bindings._bind}, { ${content} })` : `{ ${content} }`
if (!vfor) {
return `[${data}]`
Expand Down
35 changes: 31 additions & 4 deletions packages/nuxt/test/islandTransform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ describe('islandTransform - server and island components', () => {
<div>
<NuxtTeleportSsrSlot name="default" :props="undefined"><slot /></NuxtTeleportSsrSlot>

<NuxtTeleportSsrSlot name="named" :props="[{ some-data: someData }]"><slot name="named" :some-data="someData" /></NuxtTeleportSsrSlot>
<NuxtTeleportSsrSlot name="other" :props="[{ some-data: someData }]"><slot
<NuxtTeleportSsrSlot name="named" :props="[{ [\`some-data\`]: someData }]"><slot name="named" :some-data="someData" /></NuxtTeleportSsrSlot>
<NuxtTeleportSsrSlot name="other" :props="[{ [\`some-data\`]: someData }]"><slot
name="other"
:some-data="someData"
/></NuxtTeleportSsrSlot>
Expand Down Expand Up @@ -99,7 +99,7 @@ describe('islandTransform - server and island components', () => {
expect(normalizeLineEndings(result)).toMatchInlineSnapshot(`
"<template>
<div>
<NuxtTeleportSsrSlot name="default" :props="[{ some-data: someData }]"><slot :some-data="someData" /><template #fallback>
<NuxtTeleportSsrSlot name="default" :props="[{ [\`some-data\`]: someData }]"><slot :some-data="someData"/><template #fallback>
<div>fallback</div>
</template></NuxtTeleportSsrSlot>
</div>
Expand Down Expand Up @@ -158,7 +158,7 @@ describe('islandTransform - server and island components', () => {
<p>message: {{ message }}</p>
<p>Below is the slot I want to be hydrated on the client</p>
<div>
<NuxtTeleportSsrSlot name="default" :props="undefined"><slot /><template #fallback>
<NuxtTeleportSsrSlot name="default" :props="undefined"><slot/><template #fallback>
This is the default content of the slot, I should not see this after
the client loading has completed.
</template></NuxtTeleportSsrSlot>
Expand All @@ -183,6 +183,33 @@ describe('islandTransform - server and island components', () => {
"
`)
})

it('expect v-if/v-else/v-else-if to be set in teleport component wrapper', async () => {
const result = await viteTransform(`<script setup lang="ts">
const foo = true;
</script>
<template>
<slot v-if="foo" />
<slot v-else-if="test" />
<slot v-else />
</template>
`, 'WithVif.vue', false, true)

expect(normalizeLineEndings(result)).toMatchInlineSnapshot(`
"<script setup lang="ts">
import { vforToArray as __vforToArray } from '#app/components/utils'
import NuxtTeleportIslandComponent from '#app/components/nuxt-teleport-island-component'
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'
const foo = true;
</script>
<template>
<NuxtTeleportSsrSlot v-if="foo" name="default" :props="undefined"><slot /></NuxtTeleportSsrSlot>
<NuxtTeleportSsrSlot v-else-if="test" name="default" :props="undefined"><slot /></NuxtTeleportSsrSlot>
<NuxtTeleportSsrSlot v-else name="default" :props="undefined"><slot /></NuxtTeleportSsrSlot>
</template>
"
`)
})
})

describe('nuxt-client', () => {
Expand Down