Skip to content

Commit dd884ee

Browse files
authored
Merge pull request #2198 from dos1in/feat/cross-platform-warning
feat: 添加跨平台语法检测功能,发出使用不当前缀的警告
2 parents 150cee0 + 01b5ec3 commit dd884ee

File tree

4 files changed

+313
-2
lines changed

4 files changed

+313
-2
lines changed

docs-vitepress/guide/basic/start.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ mpx-cli-service build --targets=wx,ali
8989
| QQ | qq |
9090
| 头条 | tt |
9191
| 浏览器 | web |
92+
| 快应用 | qa |
9293
| 安卓 | android |
9394
| iOS | ios |
9495
| 鸿蒙 | harmony |

packages/webpack-plugin/lib/template-compiler/compiler.js

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const { isNonPhrasingTag } = require('../utils/dom-tag-config')
1515
const setBaseWxml = require('../runtime-render/base-wxml')
1616
const { parseExp } = require('./parse-exps')
1717
const shallowStringify = require('../utils/shallow-stringify')
18-
const { isReact, isWeb } = require('../utils/env')
18+
const { isReact, isWeb, isNoMode } = require('../utils/env')
1919
const { capitalToHyphen } = require('../utils/string')
2020

2121
const no = function () {
@@ -120,6 +120,8 @@ const rulesResultMap = new Map()
120120
let usingComponents = []
121121
let usingComponentsInfo = {}
122122
let componentGenerics = {}
123+
// 跨平台语法检测的配置,在模块加载时初始化一次
124+
let crossPlatformConfig = null
123125

124126
function updateForScopesMap () {
125127
forScopesMap = {}
@@ -638,6 +640,8 @@ function parse (template, options) {
638640
processingTemplate = false
639641
rulesResultMap.clear()
640642
componentGenerics = options.componentGenerics || {}
643+
// 初始化跨平台语法检测配置(每次解析时只初始化一次)
644+
crossPlatformConfig = initCrossPlatformConfig()
641645

642646
usingComponents = Object.keys(options.usingComponentsInfo)
643647
usingComponentsInfo = options.usingComponentsInfo
@@ -2851,6 +2855,78 @@ function processNoTransAttrs (el) {
28512855
}
28522856
}
28532857

2858+
function initCrossPlatformConfig () {
2859+
// 定义平台与前缀的双向映射关系
2860+
const platformPrefixMap = {
2861+
wx: 'wx:',
2862+
ali: 'a:',
2863+
swan: 's-',
2864+
qq: 'qq:',
2865+
tt: 'tt:',
2866+
dd: 'dd:',
2867+
jd: 'jd:',
2868+
qa: 'qa:',
2869+
web: 'v-'
2870+
}
2871+
2872+
if (isNoMode(mode)) {
2873+
return null
2874+
}
2875+
2876+
return {
2877+
currentPrefix: platformPrefixMap[mode] || 'wx:',
2878+
platformPrefixMap
2879+
}
2880+
}
2881+
2882+
// 检测跨平台语法使用情况并给出警告
2883+
function processCrossPlatformSyntaxWarning (el) {
2884+
// 使用转换后的属性列表进行检查
2885+
if (!el.attrsList || el.attrsList.length === 0) {
2886+
return
2887+
}
2888+
2889+
// 如果配置为空,说明不需要检测
2890+
if (!crossPlatformConfig) {
2891+
return
2892+
}
2893+
2894+
const { currentPrefix, platformPrefixMap } = crossPlatformConfig
2895+
2896+
// 检查转换后的属性列表
2897+
el.attrsList.forEach(attr => {
2898+
const attrName = attr.name
2899+
2900+
// 检查是否使用了平台前缀
2901+
for (const [platformName, prefix] of Object.entries(platformPrefixMap)) {
2902+
if (attrName.startsWith(prefix)) {
2903+
if (isReact(mode)) {
2904+
// React Native 平台:只允许使用 wx: 前缀,其他前缀报错
2905+
if (prefix !== 'wx:') {
2906+
error$1(
2907+
`React Native mode "${mode}" does not support "${prefix}" prefix. ` +
2908+
`Use "wx:" prefix instead. Found: "${attrName}"`
2909+
)
2910+
}
2911+
} else {
2912+
// 小程序平台:检测跨平台语法使用
2913+
if (platformName !== mode) {
2914+
// 构建建议的正确属性名
2915+
const suffixPart = attrName.substring(prefix.length)
2916+
const suggestedAttr = currentPrefix + suffixPart
2917+
2918+
warn$1(
2919+
`Your target mode is "${mode}", but used "${attrName}". ` +
2920+
`Did you mean "${suggestedAttr}"?`
2921+
)
2922+
}
2923+
}
2924+
break
2925+
}
2926+
}
2927+
})
2928+
}
2929+
28542930
function processMpxTagName (el) {
28552931
const mpxTagName = getAndRemoveAttr(el, 'mpxTagName').val
28562932
if (mpxTagName) {
@@ -2880,6 +2956,9 @@ function processElement (el, root, options, meta) {
28802956

28812957
processDuplicateAttrsList(el)
28822958

2959+
// 检测跨平台语法使用情况并给出警告
2960+
processCrossPlatformSyntaxWarning(el)
2961+
28832962
processInjectWxs(el, meta, options)
28842963

28852964
const transAli = mode === 'ali' && srcMode === 'wx'

packages/webpack-plugin/lib/utils/env.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ function isMiniProgram (mode) {
1010
return !isWeb(mode) && !isReact(mode)
1111
}
1212

13+
function isNoMode (mode) {
14+
return mode === 'noMode'
15+
}
16+
1317
module.exports = {
1418
isWeb,
1519
isReact,
16-
isMiniProgram
20+
isMiniProgram,
21+
isNoMode
1722
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
const { compileTemplate, warnFn, errorFn } = require('../util')
2+
3+
describe('cross-platform syntax warning', function () {
4+
afterEach(() => {
5+
warnFn.mockClear()
6+
errorFn.mockClear()
7+
})
8+
9+
it('should not warn for auto-converted attributes (wx:class in ali mode)', function () {
10+
// wx:class 会被平台转换规则自动转换为 a:class,所以不会警告
11+
const input = '<view wx:class="{{someClass}}">content</view>'
12+
compileTemplate(input, { mode: 'ali' })
13+
expect(warnFn).not.toHaveBeenCalled()
14+
})
15+
16+
it('should warn when using a: prefix in wx mode (no auto-conversion)', function () {
17+
// a:class 在 wx 模式下没有转换规则,会触发警告
18+
const input = '<view a:class="{{someClass}}">content</view>'
19+
compileTemplate(input, { mode: 'wx' })
20+
expect(warnFn).toHaveBeenCalledWith(
21+
expect.stringContaining('Your target mode is "wx", but used "a:class". Did you mean "wx:class"?')
22+
)
23+
})
24+
25+
it('should warn when using s- prefix in wx mode (no auto-conversion)', function () {
26+
// s-if 在 wx 模式下没有转换规则,会触发警告
27+
const input = '<view s-if="{{condition}}">content</view>'
28+
compileTemplate(input, { mode: 'wx' })
29+
expect(warnFn).toHaveBeenCalledWith(
30+
expect.stringContaining('Your target mode is "wx", but used "s-if". Did you mean "wx:if"?')
31+
)
32+
})
33+
34+
it('should warn when using qq: prefix in ali mode (no auto-conversion)', function () {
35+
// qq:for 在 ali 模式下没有转换规则,会触发警告
36+
const input = '<view qq:for="{{items}}">content</view>'
37+
compileTemplate(input, { mode: 'ali' })
38+
expect(warnFn).toHaveBeenCalledWith(
39+
expect.stringContaining('Your target mode is "ali", but used "qq:for". Did you mean "a:for"?')
40+
)
41+
})
42+
43+
it('should warn when using tt: prefix in swan mode (no auto-conversion)', function () {
44+
// tt:style 在 swan 模式下没有转换规则,会触发警告
45+
const input = '<view tt:style="{{dynamicStyle}}">content</view>'
46+
compileTemplate(input, { mode: 'swan' })
47+
expect(warnFn).toHaveBeenCalledWith(
48+
expect.stringContaining('Your target mode is "swan", but used "tt:style". Did you mean "s-style"?')
49+
)
50+
})
51+
52+
it('should warn when using dd: prefix in qq mode (no auto-conversion)', function () {
53+
// dd:show 在 qq 模式下没有转换规则,会触发警告
54+
const input = '<view dd:show="{{visible}}">content</view>'
55+
compileTemplate(input, { mode: 'qq' })
56+
expect(warnFn).toHaveBeenCalledWith(
57+
expect.stringContaining('Your target mode is "qq", but used "dd:show". Did you mean "qq:show"?')
58+
)
59+
})
60+
61+
it('should not warn when using correct prefix for current mode', function () {
62+
const input1 = '<view wx:class="{{someClass}}">content</view>'
63+
compileTemplate(input1, { mode: 'wx' })
64+
expect(warnFn).not.toHaveBeenCalled()
65+
66+
const input2 = '<view a:class="{{someClass}}">content</view>'
67+
compileTemplate(input2, { mode: 'ali' })
68+
expect(warnFn).not.toHaveBeenCalled()
69+
70+
const input3 = '<view s-if="{{condition}}">content</view>'
71+
compileTemplate(input3, { mode: 'swan' })
72+
expect(warnFn).not.toHaveBeenCalled()
73+
74+
const input4 = '<view qq:for="{{items}}">content</view>'
75+
compileTemplate(input4, { mode: 'qq' })
76+
expect(warnFn).not.toHaveBeenCalled()
77+
78+
const input5 = '<view tt:style="{{dynamicStyle}}">content</view>'
79+
compileTemplate(input5, { mode: 'tt' })
80+
expect(warnFn).not.toHaveBeenCalled()
81+
})
82+
83+
it('should not warn for regular attributes without platform prefixes', function () {
84+
const input = '<view class="normal" style="color: red;" id="test">content</view>'
85+
compileTemplate(input, { mode: 'ali' })
86+
expect(warnFn).not.toHaveBeenCalled()
87+
})
88+
89+
it('should warn for cross-platform attributes without auto-conversion', function () {
90+
// 只检测没有自动转换规则的属性
91+
const input = '<view s-if="{{condition}}" qq:for="{{items}}">content</view>'
92+
compileTemplate(input, { mode: 'ali' })
93+
expect(warnFn).toHaveBeenCalledTimes(2)
94+
expect(warnFn).toHaveBeenCalledWith(
95+
expect.stringContaining('Your target mode is "ali", but used "s-if". Did you mean "a:if"?')
96+
)
97+
expect(warnFn).toHaveBeenCalledWith(
98+
expect.stringContaining('Your target mode is "ali", but used "qq:for". Did you mean "a:for"?')
99+
)
100+
})
101+
102+
it('should work with nested elements', function () {
103+
// 只测试没有自动转换的属性
104+
const input = `
105+
<view s-if="{{condition}}">
106+
<text s-class="{{textClass}}">Hello</text>
107+
</view>
108+
`
109+
compileTemplate(input, { mode: 'ali' })
110+
expect(warnFn).toHaveBeenCalledTimes(2)
111+
expect(warnFn).toHaveBeenCalledWith(
112+
expect.stringContaining('Your target mode is "ali", but used "s-if". Did you mean "a:if"?')
113+
)
114+
expect(warnFn).toHaveBeenCalledWith(
115+
expect.stringContaining('Your target mode is "ali", but used "s-class". Did you mean "a:class"?')
116+
)
117+
})
118+
119+
it('should handle all supported platforms', function () {
120+
// Test jd: prefix
121+
const input1 = '<view jd:ref="myRef">content</view>'
122+
compileTemplate(input1, { mode: 'wx' })
123+
expect(warnFn).toHaveBeenCalledWith(
124+
expect.stringContaining('Your target mode is "wx", but used "jd:ref". Did you mean "wx:ref"?')
125+
)
126+
127+
warnFn.mockClear()
128+
129+
// Test qa: prefix
130+
const input2 = '<view qa:key="{{item.id}}">content</view>'
131+
compileTemplate(input2, { mode: 'tt' })
132+
expect(warnFn).toHaveBeenCalledWith(
133+
expect.stringContaining('Your target mode is "tt", but used "qa:key". Did you mean "tt:key"?')
134+
)
135+
})
136+
137+
it('should error for React Native platforms when using non-wx prefixes', function () {
138+
const input = '<view a:class="{{someClass}}">content</view>'
139+
140+
// Test android
141+
compileTemplate(input, { mode: 'android' })
142+
expect(errorFn).toHaveBeenCalledWith(
143+
expect.stringContaining('React Native mode "android" does not support "a:" prefix. Use "wx:" prefix instead. Found: "a:class"')
144+
)
145+
146+
errorFn.mockClear()
147+
148+
// Test ios
149+
compileTemplate(input, { mode: 'ios' })
150+
expect(errorFn).toHaveBeenCalledWith(
151+
expect.stringContaining('React Native mode "ios" does not support "a:" prefix. Use "wx:" prefix instead. Found: "a:class"')
152+
)
153+
154+
errorFn.mockClear()
155+
156+
// Test harmony
157+
compileTemplate(input, { mode: 'harmony' })
158+
expect(errorFn).toHaveBeenCalledWith(
159+
expect.stringContaining('React Native mode "harmony" does not support "a:" prefix. Use "wx:" prefix instead. Found: "a:class"')
160+
)
161+
})
162+
163+
it('should not warn for React Native platforms when using wx: prefix', function () {
164+
const input = '<view wx:class="{{someClass}}">content</view>'
165+
166+
// Test android
167+
compileTemplate(input, { mode: 'android' })
168+
expect(warnFn).not.toHaveBeenCalled()
169+
expect(errorFn).not.toHaveBeenCalled()
170+
171+
// Test ios
172+
compileTemplate(input, { mode: 'ios' })
173+
expect(warnFn).not.toHaveBeenCalled()
174+
expect(errorFn).not.toHaveBeenCalled()
175+
176+
// Test harmony
177+
compileTemplate(input, { mode: 'harmony' })
178+
expect(warnFn).not.toHaveBeenCalled()
179+
expect(errorFn).not.toHaveBeenCalled()
180+
})
181+
182+
it('should handle conditional compilation attributes correctly', function () {
183+
// 测试 wx to ali 场景下的条件编译属性
184+
const input1 = '<alicom@ali a:if="{{show}}">content</alicom>'
185+
compileTemplate(input1, { srcMode: 'wx', mode: 'ali' })
186+
expect(warnFn).not.toHaveBeenCalled() // 应该不警告,因为目标是ali模式
187+
188+
const input2 = '<com a:if@ali="{{show}}">content</com>'
189+
compileTemplate(input2, { srcMode: 'wx', mode: 'ali' })
190+
expect(warnFn).not.toHaveBeenCalled() // 应该不警告,因为目标是ali模式
191+
})
192+
193+
it('should warn when using a: prefix in web mode', function () {
194+
// web 模式应该使用 v- 前缀,使用 a: 会警告
195+
const input = '<view a:class="someClass">content</view>'
196+
compileTemplate(input, { mode: 'web' })
197+
expect(warnFn).toHaveBeenCalledWith(
198+
expect.stringContaining('Your target mode is "web", but used "a:class". Did you mean "v-class"?')
199+
)
200+
})
201+
202+
it('should warn when using s- prefix in web mode', function () {
203+
// web 模式应该使用 v- 前缀,使用 s- 会警告
204+
const input = '<view s-if="condition">content</view>'
205+
compileTemplate(input, { mode: 'web' })
206+
expect(warnFn).toHaveBeenCalledWith(
207+
expect.stringContaining('Your target mode is "web", but used "s-if". Did you mean "v-if"?')
208+
)
209+
})
210+
211+
it('should not warn when using v- prefix in web mode', function () {
212+
// web 模式使用 v- 前缀不应该警告
213+
const input = '<div v-if="condition">content</div>'
214+
compileTemplate(input, { mode: 'web' })
215+
expect(warnFn).not.toHaveBeenCalled()
216+
})
217+
218+
it('should warn when using v- prefix in miniprogram mode', function () {
219+
// 小程序模式使用 v- 前缀应该警告
220+
const input = '<view v-if="condition">content</view>'
221+
compileTemplate(input, { mode: 'wx' })
222+
expect(warnFn).toHaveBeenCalledWith(
223+
expect.stringContaining('Your target mode is "wx", but used "v-if". Did you mean "wx:if"?')
224+
)
225+
})
226+
})

0 commit comments

Comments
 (0)