import { pathToFileURL } from 'node:url' import type { Component } from '@nuxt/schema' import { parseURL } from 'ufo' import { createUnplugin } from 'unplugin' import MagicString from 'magic-string' import { ELEMENT_NODE, parse, walk } from 'ultrahtml' import { isVue } from '../core/utils' interface ServerOnlyComponentTransformPluginOptions { getComponents: () => Component[] } const SCRIPT_RE = /]*>/g const HAS_SLOT_RE = /([\s\S]*)<\/template>/ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTransformPluginOptions) => { return { name: 'server-only-component-transform', enforce: 'pre', transformInclude (id) { if (!isVue(id)) { return false } const components = options.getComponents() const islands = components.filter(component => component.island || (component.mode === 'server' && !components.some(c => c.pascalName === component.pascalName && c.mode === 'client')) ) const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href)) return islands.some(c => c.filePath === pathname) }, async transform (code, id) { if (!HAS_SLOT_RE.test(code)) { return } const template = code.match(TEMPLATE_RE) if (!template) { return } const startingIndex = template.index || 0 const s = new MagicString(code) s.replace(SCRIPT_RE, (full) => { return full + '\nimport { vforToArray as __vforToArray } from \'#app/components/utils\'' }) const ast = parse(template[0]) await walk(ast, (node) => { if (node.type === ELEMENT_NODE && node.name === 'slot') { const { attributes, children, loc, isSelfClosingTag } = node const slotName = attributes.name ?? 'default' let vfor: [string, string] | undefined if (attributes['v-for']) { vfor = attributes['v-for'].split(' in ').map((v: string) => v.trim()) as [string, string] delete attributes['v-for'] } if (attributes.name) { delete attributes.name } if (attributes['v-bind']) { attributes._bind = attributes['v-bind'] delete attributes['v-bind'] } const bindings = getBindings(attributes, vfor) if (isSelfClosingTag) { s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, `
`) } else { s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, `
`) s.overwrite(startingIndex + loc[1].start, startingIndex + loc[1].end, '
') if (children.length > 1) { // need to wrap instead of applying v-for on each child const wrapperTag = `
` s.appendRight(startingIndex + loc[0].end, `
${wrapperTag}`) s.appendLeft(startingIndex + loc[1].start, '
') } else if (children.length === 1) { if (vfor && children[0].type === ELEMENT_NODE) { const { loc, name, attributes, isSelfClosingTag } = children[0] const attrs = Object.entries(attributes).map(([attr, val]) => `${attr}="${val}"`).join(' ') s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, `<${name} v-for="${vfor[0]} in ${vfor[1]}" ${attrs} ${isSelfClosingTag ? '/' : ''}>`) } s.appendRight(startingIndex + loc[0].end, `
`) s.appendLeft(startingIndex + loc[1].start, '
') } } } }) if (s.hasChanged()) { return { code: s.toString(), map: s.generateMap({ source: id, includeContent: true }) } } } } }) function isBinding (attr: string): boolean { return attr.startsWith(':') } function getBindings (bindings: Record, vfor?: [string, string]): string { if (Object.keys(bindings).length === 0) { return '' } const content = Object.entries(bindings).filter(b => 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 `:nuxt-ssr-slot-data="JSON.stringify([${data}])"` } else { return `:nuxt-ssr-slot-data="JSON.stringify(__vforToArray(${vfor[1]}).map(${vfor[0]} => (${data})))"` } }