Skip to content

Commit 38cc8ae

Browse files
zyyvCopilot
andauthored
fix(transformer-attributify-jsx): falls back to regular expression when babel parse error (#5032)
Co-authored-by: Copilot <[email protected]>
1 parent 5e93d5b commit 38cc8ae

File tree

3 files changed

+125
-33
lines changed

3 files changed

+125
-33
lines changed

packages-presets/transformer-attributify-jsx/src/index.ts

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import type { SourceCodeTransformer } from '@unocss/core'
1+
import type { SourceCodeTransformer, UnoGenerator } from '@unocss/core'
2+
import type MagicString from 'magic-string'
23
import { getEnvFlags } from '#integration/env'
3-
import { parse } from '@babel/parser'
4-
import _traverse from '@babel/traverse'
54
import { toArray } from '@unocss/core'
6-
7-
// @ts-expect-error ignore
8-
const traverse = (_traverse.default || _traverse) as typeof _traverse
5+
import { attributifyJsxBabelResolver } from './resolver/babel'
6+
import { attributifyJsxRegexResolver } from './resolver/regex'
97

108
export type FilterPattern = Array<string | RegExp> | string | RegExp | null
119

@@ -43,6 +41,13 @@ export interface TransformerAttributifyJsxOptions {
4341
exclude?: FilterPattern
4442
}
4543

44+
export interface AttributifyResolverParams {
45+
code: MagicString
46+
id: string
47+
uno: UnoGenerator<object>
48+
isBlocked: (matchedRule: string) => boolean
49+
}
50+
4651
export default function transformerAttributifyJsx(options: TransformerAttributifyJsxOptions = {}): SourceCodeTransformer {
4752
const {
4853
blocklist = [],
@@ -71,7 +76,7 @@ export default function transformerAttributifyJsx(options: TransformerAttributif
7176
name: '@unocss/transformer-attributify-jsx',
7277
enforce: 'pre',
7378
idFilter,
74-
async transform(code, _, { uno }) {
79+
async transform(code, id, { uno }) {
7580
// Skip if running in VSCode extension context
7681
try {
7782
if (getEnvFlags().isVSCode)
@@ -80,34 +85,24 @@ export default function transformerAttributifyJsx(options: TransformerAttributif
8085
catch {
8186
// Ignore import error in browser environment
8287
}
83-
const tasks: Promise<void>[] = []
84-
const ast = parse(code.toString(), {
85-
sourceType: 'module',
86-
plugins: ['jsx', 'typescript'],
87-
})
88-
89-
traverse(ast, {
90-
JSXAttribute(path) {
91-
if (path.node.value === null) {
92-
const attr = path.node.name.type === 'JSXNamespacedName'
93-
? `${path.node.name.namespace.name}:${path.node.name.name.name}`
94-
: path.node.name.name
95-
96-
if (isBlocked(attr))
97-
return
9888

99-
tasks.push(
100-
uno.parseToken(attr).then((matched) => {
101-
if (matched) {
102-
code.appendRight(path.node.end!, '=""')
103-
}
104-
}),
105-
)
106-
}
107-
},
108-
})
89+
const params: AttributifyResolverParams = {
90+
code,
91+
id,
92+
uno,
93+
isBlocked,
94+
}
10995

110-
await Promise.all(tasks)
96+
try {
97+
await attributifyJsxBabelResolver(params)
98+
}
99+
catch (error) {
100+
console.warn(
101+
`[@unocss/transformer-attributify-jsx]: Babel resolver failed for "${id}", falling back to regex resolver:`,
102+
error,
103+
)
104+
await attributifyJsxRegexResolver(params)
105+
}
111106
},
112107
}
113108
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { AttributifyResolverParams } from '..'
2+
import { parse } from '@babel/parser'
3+
import _traverse from '@babel/traverse'
4+
5+
// @ts-expect-error ignore
6+
const traverse = (_traverse.default || _traverse) as typeof _traverse
7+
8+
export async function attributifyJsxBabelResolver(params: AttributifyResolverParams) {
9+
const { code, uno, isBlocked } = params
10+
const tasks: Promise<void>[] = []
11+
const ast = parse(code.toString(), {
12+
sourceType: 'module',
13+
plugins: ['jsx', 'typescript'],
14+
})
15+
16+
if (ast.errors?.length) {
17+
throw new Error(`Babel parse errors:\n${ast.errors.join('\n')}`)
18+
}
19+
20+
traverse(ast, {
21+
JSXAttribute(path) {
22+
if (path.node.value === null) {
23+
const attr = path.node.name.type === 'JSXNamespacedName'
24+
? `${path.node.name.namespace.name}:${path.node.name.name.name}`
25+
: path.node.name.name
26+
27+
if (isBlocked(attr))
28+
return
29+
30+
tasks.push(
31+
uno.parseToken(attr).then((matched) => {
32+
if (matched) {
33+
code.appendRight(path.node.end!, '=""')
34+
}
35+
}),
36+
)
37+
}
38+
},
39+
})
40+
41+
await Promise.all(tasks)
42+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { AttributifyResolverParams } from '..'
2+
3+
// eslint-disable-next-line regexp/no-super-linear-backtracking
4+
const elementRE = /<([^/?<>0-9$_!][^\s>]*)\s+((?:"[^"]*"|'[^"]*'|(\{[^}]*\})|[^{>])+)>/g
5+
const attributeRE = /(?<![~`!$%^&*()_+\-=[{;':"|,.<>/?])([a-z()#][[?\w\-:()#%\]]*)(?:\s*=\s*('[^']*'|"[^"]*"|\S+))?|\{[^}]*\}/gi
6+
// eslint-disable-next-line regexp/no-super-linear-backtracking
7+
const valuedAttributeRE = /((?!\d|-{2}|-\d)[\w\u00A0-\uFFFF:!%.~<-]+)=(?:"[^"]*"|'[^']*'|(\{)((?:[`(][^`)]*[`)]|[^}])+)(\}))/g
8+
9+
export async function attributifyJsxRegexResolver(params: AttributifyResolverParams) {
10+
const { code, uno, isBlocked } = params
11+
const tasks: Promise<void>[] = []
12+
const attributify = uno.config.presets.find(i => i.name === '@unocss/preset-attributify')
13+
const attributifyPrefix = attributify?.options?.prefix ?? 'un-'
14+
for (const item of Array.from(code.original.matchAll(elementRE))) {
15+
// Extract the JSX attributes portion and mask complex valued attributes with whitespace for attributify processing
16+
let attributifyPart = item[2]
17+
if (valuedAttributeRE.test(attributifyPart)) {
18+
attributifyPart = attributifyPart.replace(valuedAttributeRE, (match, _, dynamicFlagStart) => {
19+
if (!dynamicFlagStart)
20+
return ' '.repeat(match.length)
21+
let preLastModifierIndex = 0
22+
let temp = match
23+
// No more recursive processing of the more complex situations of JSX in attributes.
24+
for (const _item of match.matchAll(elementRE)) {
25+
const attrAttributePart = _item[2]
26+
if (valuedAttributeRE.test(attrAttributePart))
27+
attrAttributePart.replace(valuedAttributeRE, (m: string) => ' '.repeat(m.length))
28+
29+
const pre = temp.slice(0, preLastModifierIndex) + ' '.repeat(_item.index + _item[0].indexOf(_item[2]) - preLastModifierIndex) + attrAttributePart
30+
temp = pre + ' '.repeat(_item.input.length - pre.length)
31+
preLastModifierIndex = pre.length
32+
}
33+
if (preLastModifierIndex !== 0)
34+
return temp
35+
36+
return ' '.repeat(match.length)
37+
})
38+
}
39+
for (const attr of attributifyPart.matchAll(attributeRE)) {
40+
const matchedRule = attr[0]
41+
if (matchedRule.includes('=') || isBlocked(matchedRule))
42+
continue
43+
const updatedMatchedRule = matchedRule.startsWith(attributifyPrefix) ? matchedRule.slice(attributifyPrefix.length) : matchedRule
44+
tasks.push(uno.parseToken(updatedMatchedRule).then((matched) => {
45+
if (matched) {
46+
const startIdx = (item.index || 0) + (attr.index || 0) + item[0].indexOf(item[2])
47+
const endIdx = startIdx + matchedRule.length
48+
code.overwrite(startIdx, endIdx, `${matchedRule}=""`)
49+
}
50+
}))
51+
}
52+
}
53+
54+
await Promise.all(tasks)
55+
}

0 commit comments

Comments
 (0)