2022-07-14 17:46:12 +00:00
|
|
|
import { pathToFileURL } from 'node:url'
|
|
|
|
import { parseURL } from 'ufo'
|
|
|
|
import MagicString from 'magic-string'
|
2022-12-11 21:44:52 +00:00
|
|
|
import type { Node } from 'ultrahtml'
|
|
|
|
import { parse, walk, ELEMENT_NODE } from 'ultrahtml'
|
2022-07-14 17:46:12 +00:00
|
|
|
import { createUnplugin } from 'unplugin'
|
|
|
|
import type { Component } from '@nuxt/schema'
|
|
|
|
|
|
|
|
interface TreeShakeTemplatePluginOptions {
|
|
|
|
sourcemap?: boolean
|
2022-09-20 06:24:45 +00:00
|
|
|
getComponents (): Component[]
|
2022-07-14 17:46:12 +00:00
|
|
|
}
|
|
|
|
|
2022-10-10 15:48:23 +00:00
|
|
|
const PLACEHOLDER_RE = /^(v-slot|#)(fallback|placeholder)/
|
|
|
|
|
2022-07-14 17:46:12 +00:00
|
|
|
export const TreeShakeTemplatePlugin = createUnplugin((options: TreeShakeTemplatePluginOptions) => {
|
2022-10-10 15:48:23 +00:00
|
|
|
const regexpMap = new WeakMap<Component[], [RegExp, string[]]>()
|
2022-07-14 17:46:12 +00:00
|
|
|
return {
|
|
|
|
name: 'nuxt:tree-shake-template',
|
|
|
|
enforce: 'pre',
|
|
|
|
transformInclude (id) {
|
|
|
|
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
|
|
|
return pathname.endsWith('.vue')
|
|
|
|
},
|
2022-10-10 15:48:23 +00:00
|
|
|
async transform (code, id) {
|
|
|
|
const template = code.match(/<template>([\s\S]*)<\/template>/)
|
|
|
|
if (!template) { return }
|
|
|
|
|
2022-07-14 17:46:12 +00:00
|
|
|
const components = options.getComponents()
|
|
|
|
|
|
|
|
if (!regexpMap.has(components)) {
|
|
|
|
const clientOnlyComponents = components
|
|
|
|
.filter(c => c.mode === 'client' && !components.some(other => other.mode !== 'client' && other.pascalName === c.pascalName))
|
2022-10-10 15:48:23 +00:00
|
|
|
.flatMap(c => [c.pascalName, c.kebabName])
|
|
|
|
.concat(['ClientOnly', 'client-only'])
|
|
|
|
const tags = clientOnlyComponents
|
|
|
|
.map(component => `<(${component})[^>]*>[\\s\\S]*?<\\/(${component})>`)
|
2022-07-14 17:46:12 +00:00
|
|
|
|
2022-10-10 15:48:23 +00:00
|
|
|
regexpMap.set(components, [new RegExp(`(${tags.join('|')})`, 'g'), clientOnlyComponents])
|
2022-07-14 17:46:12 +00:00
|
|
|
}
|
|
|
|
|
2022-10-10 15:48:23 +00:00
|
|
|
const [COMPONENTS_RE, clientOnlyComponents] = regexpMap.get(components)!
|
|
|
|
if (!COMPONENTS_RE.test(code)) { return }
|
|
|
|
|
2022-07-14 17:46:12 +00:00
|
|
|
const s = new MagicString(code)
|
|
|
|
|
2022-10-10 15:48:23 +00:00
|
|
|
const ast = parse(template[0])
|
|
|
|
await walk(ast, (node) => {
|
|
|
|
if (node.type !== ELEMENT_NODE || !clientOnlyComponents.includes(node.name) || !node.children?.length) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const fallback = node.children.find(
|
|
|
|
(n: Node) => n.name === 'template' &&
|
|
|
|
Object.entries(n.attributes as Record<string, string>)?.flat().some(attr => PLACEHOLDER_RE.test(attr))
|
|
|
|
)
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Replace node content
|
2022-10-17 10:23:51 +00:00
|
|
|
const text = fallback ? code.slice(template.index! + fallback.loc[0].start, template.index! + fallback.loc[fallback.loc.length - 1].end) : ''
|
|
|
|
s.overwrite(template.index! + node.loc[0].end, template.index! + node.loc[node.loc.length - 1].start, text)
|
2022-10-10 15:48:23 +00:00
|
|
|
} catch (err) {
|
|
|
|
// This may fail if we have a nested client-only component and are trying
|
|
|
|
// to replace some text that has already been replaced
|
|
|
|
}
|
|
|
|
})
|
2022-07-14 17:46:12 +00:00
|
|
|
|
|
|
|
if (s.hasChanged()) {
|
|
|
|
return {
|
|
|
|
code: s.toString(),
|
2022-08-22 10:12:02 +00:00
|
|
|
map: options.sourcemap
|
|
|
|
? s.generateMap({ source: id, includeContent: true })
|
|
|
|
: undefined
|
2022-07-14 17:46:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|