/
shadow-dom.ts
125 lines (115 loc) · 4.15 KB
/
shadow-dom.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import type { Plugin } from 'vite'
import type { UnocssPluginContext } from '@unocss/core'
import { CSS_PLACEHOLDER } from '../integration'
export function ShadowDomModuleModePlugin({ uno }: UnocssPluginContext): Plugin {
const partExtractorRegex = /^part-\[(.+)]:/
const nameRegexp = /<([^\s^!>]+)\s*([^>]*)>/
const vueSFCStyleRE = new RegExp(`<style.*>[\\s\\S]*${CSS_PLACEHOLDER}[\\s\\S]*<\\/style>`)
interface PartData {
part: string
rule: string
}
const checkElement = (useParts: PartData[], idxResolver: (name: string) => number, element: RegExpExecArray | null) => {
if (!element)
return null
const applyParts = useParts.filter(p => element[2].includes(p.rule))
if (applyParts.length === 0)
return null
const name = element[1]
const idx = idxResolver(name)
return {
name,
entries: applyParts.map(({ rule, part }) => [
`.${rule.replace(/[:[\]]/g, '\\$&')}::part(${part})`,
`${name}:nth-of-type(${idx})::part(${part})`,
]),
}
}
const idxMapFactory = () => {
const elementIdxMap = new Map<string, number>()
return {
idxResolver: (name: string) => {
let idx = elementIdxMap.get(name)
if (!idx) {
idx = 1
elementIdxMap.set(name, idx)
}
return idx
},
incrementIdx: (name: string) => {
elementIdxMap.set(name, elementIdxMap.get(name)! + 1)
},
}
}
const transformWebComponent = async (code: string, id: string) => {
if (!code.match(CSS_PLACEHOLDER))
return code
// eslint-disable-next-line prefer-const
let { css, matched } = await uno.generate(code, {
preflights: true,
safelist: true,
})
if (css && matched) {
// filter only parts from the result reported from the generator
const useParts = Array.from(matched).reduce((acc, rule) => {
const matcher = rule.match(partExtractorRegex)
if (matcher)
acc.push({ part: matcher[1], rule })
return acc
}, new Array<PartData>())
if (useParts.length > 0) {
let useCode = code
let element: RegExpExecArray | null
const partsToApply = new Map<string, Array<string>>()
const { idxResolver, incrementIdx } = idxMapFactory()
// We need to traverse the code to find the web components using the original class/attr part.
// We need traverse the code to apply the same order the components are on the code: we are using nth-of-type.
// A web component can have multiple parts, and so, we need to collect all of them: see checkElement above.
// eslint-disable-next-line no-cond-assign
while (element = nameRegexp.exec(useCode)) {
const result = checkElement(
useParts,
idxResolver,
element,
)
if (result) {
result.entries.forEach(([name, replacement]) => {
let list = partsToApply.get(name)
if (!list) {
list = []
partsToApply.set(name, list)
}
list.push(replacement)
})
incrementIdx(result.name)
}
useCode = useCode.slice(element[0].length + 1)
}
if (partsToApply.size > 0) {
css = Array.from(partsToApply.entries()).reduce((k, [r, name]) => {
return k.replace(r, name.join(',\n'))
}, css)
}
}
}
// We don't need to escape backslashes here, because, unlike the other
// shadow-dom targets, style block in Vue SFC is not a string literal.
if (id.includes('?vue&type=style') || (id.endsWith('.vue') && vueSFCStyleRE.test(code)))
return code.replace(new RegExp(`(\\/\\*\\s*)?${CSS_PLACEHOLDER}(\\s*\\*\\/)?`), css || '')
return code.replace(CSS_PLACEHOLDER, css?.replace(/\\/g, '\\\\') ?? '')
}
return {
name: 'unocss:shadow-dom',
enforce: 'pre',
async transform(code, id) {
return transformWebComponent(code, id)
},
handleHotUpdate(ctx) {
const read = ctx.read
ctx.read = async () => {
const code = await read()
return await transformWebComponent(code, ctx.file)
}
},
}
}