diff --git a/docs-vitepress/guide/basic/start.md b/docs-vitepress/guide/basic/start.md
index ee6b6b37df..0af9f11210 100644
--- a/docs-vitepress/guide/basic/start.md
+++ b/docs-vitepress/guide/basic/start.md
@@ -89,6 +89,7 @@ mpx-cli-service build --targets=wx,ali
| QQ | qq |
| 头条 | tt |
| 浏览器 | web |
+| 快应用 | qa |
| 安卓 | android |
| iOS | ios |
| 鸿蒙 | harmony |
diff --git a/packages/webpack-plugin/lib/template-compiler/compiler.js b/packages/webpack-plugin/lib/template-compiler/compiler.js
index 45a98627c7..27def84b66 100644
--- a/packages/webpack-plugin/lib/template-compiler/compiler.js
+++ b/packages/webpack-plugin/lib/template-compiler/compiler.js
@@ -15,7 +15,7 @@ const { isNonPhrasingTag } = require('../utils/dom-tag-config')
const setBaseWxml = require('../runtime-render/base-wxml')
const { parseExp } = require('./parse-exps')
const shallowStringify = require('../utils/shallow-stringify')
-const { isReact, isWeb } = require('../utils/env')
+const { isReact, isWeb, isNoMode } = require('../utils/env')
const { capitalToHyphen } = require('../utils/string')
const no = function () {
@@ -120,6 +120,8 @@ const rulesResultMap = new Map()
let usingComponents = []
let usingComponentsInfo = {}
let componentGenerics = {}
+// 跨平台语法检测的配置,在模块加载时初始化一次
+let crossPlatformConfig = null
function updateForScopesMap () {
forScopesMap = {}
@@ -638,6 +640,8 @@ function parse (template, options) {
processingTemplate = false
rulesResultMap.clear()
componentGenerics = options.componentGenerics || {}
+ // 初始化跨平台语法检测配置(每次解析时只初始化一次)
+ crossPlatformConfig = initCrossPlatformConfig()
usingComponents = Object.keys(options.usingComponentsInfo)
usingComponentsInfo = options.usingComponentsInfo
@@ -2851,6 +2855,78 @@ function processNoTransAttrs (el) {
}
}
+function initCrossPlatformConfig () {
+ // 定义平台与前缀的双向映射关系
+ const platformPrefixMap = {
+ wx: 'wx:',
+ ali: 'a:',
+ swan: 's-',
+ qq: 'qq:',
+ tt: 'tt:',
+ dd: 'dd:',
+ jd: 'jd:',
+ qa: 'qa:',
+ web: 'v-'
+ }
+
+ if (isNoMode(mode)) {
+ return null
+ }
+
+ return {
+ currentPrefix: platformPrefixMap[mode] || 'wx:',
+ platformPrefixMap
+ }
+}
+
+// 检测跨平台语法使用情况并给出警告
+function processCrossPlatformSyntaxWarning (el) {
+ // 使用转换后的属性列表进行检查
+ if (!el.attrsList || el.attrsList.length === 0) {
+ return
+ }
+
+ // 如果配置为空,说明不需要检测
+ if (!crossPlatformConfig) {
+ return
+ }
+
+ const { currentPrefix, platformPrefixMap } = crossPlatformConfig
+
+ // 检查转换后的属性列表
+ el.attrsList.forEach(attr => {
+ const attrName = attr.name
+
+ // 检查是否使用了平台前缀
+ for (const [platformName, prefix] of Object.entries(platformPrefixMap)) {
+ if (attrName.startsWith(prefix)) {
+ if (isReact(mode)) {
+ // React Native 平台:只允许使用 wx: 前缀,其他前缀报错
+ if (prefix !== 'wx:') {
+ error$1(
+ `React Native mode "${mode}" does not support "${prefix}" prefix. ` +
+ `Use "wx:" prefix instead. Found: "${attrName}"`
+ )
+ }
+ } else {
+ // 小程序平台:检测跨平台语法使用
+ if (platformName !== mode) {
+ // 构建建议的正确属性名
+ const suffixPart = attrName.substring(prefix.length)
+ const suggestedAttr = currentPrefix + suffixPart
+
+ warn$1(
+ `Your target mode is "${mode}", but used "${attrName}". ` +
+ `Did you mean "${suggestedAttr}"?`
+ )
+ }
+ }
+ break
+ }
+ }
+ })
+}
+
function processMpxTagName (el) {
const mpxTagName = getAndRemoveAttr(el, 'mpxTagName').val
if (mpxTagName) {
@@ -2880,6 +2956,9 @@ function processElement (el, root, options, meta) {
processDuplicateAttrsList(el)
+ // 检测跨平台语法使用情况并给出警告
+ processCrossPlatformSyntaxWarning(el)
+
processInjectWxs(el, meta, options)
const transAli = mode === 'ali' && srcMode === 'wx'
diff --git a/packages/webpack-plugin/lib/utils/env.js b/packages/webpack-plugin/lib/utils/env.js
index 4b0390fea0..45625b01c2 100644
--- a/packages/webpack-plugin/lib/utils/env.js
+++ b/packages/webpack-plugin/lib/utils/env.js
@@ -10,8 +10,13 @@ function isMiniProgram (mode) {
return !isWeb(mode) && !isReact(mode)
}
+function isNoMode (mode) {
+ return mode === 'noMode'
+}
+
module.exports = {
isWeb,
isReact,
- isMiniProgram
+ isMiniProgram,
+ isNoMode
}
diff --git a/packages/webpack-plugin/test/platform/common/cross-platform-warning.spec.js b/packages/webpack-plugin/test/platform/common/cross-platform-warning.spec.js
new file mode 100644
index 0000000000..bb3dfa50c6
--- /dev/null
+++ b/packages/webpack-plugin/test/platform/common/cross-platform-warning.spec.js
@@ -0,0 +1,226 @@
+const { compileTemplate, warnFn, errorFn } = require('../util')
+
+describe('cross-platform syntax warning', function () {
+ afterEach(() => {
+ warnFn.mockClear()
+ errorFn.mockClear()
+ })
+
+ it('should not warn for auto-converted attributes (wx:class in ali mode)', function () {
+ // wx:class 会被平台转换规则自动转换为 a:class,所以不会警告
+ const input = 'content'
+ compileTemplate(input, { mode: 'ali' })
+ expect(warnFn).not.toHaveBeenCalled()
+ })
+
+ it('should warn when using a: prefix in wx mode (no auto-conversion)', function () {
+ // a:class 在 wx 模式下没有转换规则,会触发警告
+ const input = 'content'
+ compileTemplate(input, { mode: 'wx' })
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "wx", but used "a:class". Did you mean "wx:class"?')
+ )
+ })
+
+ it('should warn when using s- prefix in wx mode (no auto-conversion)', function () {
+ // s-if 在 wx 模式下没有转换规则,会触发警告
+ const input = 'content'
+ compileTemplate(input, { mode: 'wx' })
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "wx", but used "s-if". Did you mean "wx:if"?')
+ )
+ })
+
+ it('should warn when using qq: prefix in ali mode (no auto-conversion)', function () {
+ // qq:for 在 ali 模式下没有转换规则,会触发警告
+ const input = 'content'
+ compileTemplate(input, { mode: 'ali' })
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "ali", but used "qq:for". Did you mean "a:for"?')
+ )
+ })
+
+ it('should warn when using tt: prefix in swan mode (no auto-conversion)', function () {
+ // tt:style 在 swan 模式下没有转换规则,会触发警告
+ const input = 'content'
+ compileTemplate(input, { mode: 'swan' })
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "swan", but used "tt:style". Did you mean "s-style"?')
+ )
+ })
+
+ it('should warn when using dd: prefix in qq mode (no auto-conversion)', function () {
+ // dd:show 在 qq 模式下没有转换规则,会触发警告
+ const input = 'content'
+ compileTemplate(input, { mode: 'qq' })
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "qq", but used "dd:show". Did you mean "qq:show"?')
+ )
+ })
+
+ it('should not warn when using correct prefix for current mode', function () {
+ const input1 = 'content'
+ compileTemplate(input1, { mode: 'wx' })
+ expect(warnFn).not.toHaveBeenCalled()
+
+ const input2 = 'content'
+ compileTemplate(input2, { mode: 'ali' })
+ expect(warnFn).not.toHaveBeenCalled()
+
+ const input3 = 'content'
+ compileTemplate(input3, { mode: 'swan' })
+ expect(warnFn).not.toHaveBeenCalled()
+
+ const input4 = 'content'
+ compileTemplate(input4, { mode: 'qq' })
+ expect(warnFn).not.toHaveBeenCalled()
+
+ const input5 = 'content'
+ compileTemplate(input5, { mode: 'tt' })
+ expect(warnFn).not.toHaveBeenCalled()
+ })
+
+ it('should not warn for regular attributes without platform prefixes', function () {
+ const input = 'content'
+ compileTemplate(input, { mode: 'ali' })
+ expect(warnFn).not.toHaveBeenCalled()
+ })
+
+ it('should warn for cross-platform attributes without auto-conversion', function () {
+ // 只检测没有自动转换规则的属性
+ const input = 'content'
+ compileTemplate(input, { mode: 'ali' })
+ expect(warnFn).toHaveBeenCalledTimes(2)
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "ali", but used "s-if". Did you mean "a:if"?')
+ )
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "ali", but used "qq:for". Did you mean "a:for"?')
+ )
+ })
+
+ it('should work with nested elements', function () {
+ // 只测试没有自动转换的属性
+ const input = `
+
+ Hello
+
+ `
+ compileTemplate(input, { mode: 'ali' })
+ expect(warnFn).toHaveBeenCalledTimes(2)
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "ali", but used "s-if". Did you mean "a:if"?')
+ )
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "ali", but used "s-class". Did you mean "a:class"?')
+ )
+ })
+
+ it('should handle all supported platforms', function () {
+ // Test jd: prefix
+ const input1 = 'content'
+ compileTemplate(input1, { mode: 'wx' })
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "wx", but used "jd:ref". Did you mean "wx:ref"?')
+ )
+
+ warnFn.mockClear()
+
+ // Test qa: prefix
+ const input2 = 'content'
+ compileTemplate(input2, { mode: 'tt' })
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "tt", but used "qa:key". Did you mean "tt:key"?')
+ )
+ })
+
+ it('should error for React Native platforms when using non-wx prefixes', function () {
+ const input = 'content'
+
+ // Test android
+ compileTemplate(input, { mode: 'android' })
+ expect(errorFn).toHaveBeenCalledWith(
+ expect.stringContaining('React Native mode "android" does not support "a:" prefix. Use "wx:" prefix instead. Found: "a:class"')
+ )
+
+ errorFn.mockClear()
+
+ // Test ios
+ compileTemplate(input, { mode: 'ios' })
+ expect(errorFn).toHaveBeenCalledWith(
+ expect.stringContaining('React Native mode "ios" does not support "a:" prefix. Use "wx:" prefix instead. Found: "a:class"')
+ )
+
+ errorFn.mockClear()
+
+ // Test harmony
+ compileTemplate(input, { mode: 'harmony' })
+ expect(errorFn).toHaveBeenCalledWith(
+ expect.stringContaining('React Native mode "harmony" does not support "a:" prefix. Use "wx:" prefix instead. Found: "a:class"')
+ )
+ })
+
+ it('should not warn for React Native platforms when using wx: prefix', function () {
+ const input = 'content'
+
+ // Test android
+ compileTemplate(input, { mode: 'android' })
+ expect(warnFn).not.toHaveBeenCalled()
+ expect(errorFn).not.toHaveBeenCalled()
+
+ // Test ios
+ compileTemplate(input, { mode: 'ios' })
+ expect(warnFn).not.toHaveBeenCalled()
+ expect(errorFn).not.toHaveBeenCalled()
+
+ // Test harmony
+ compileTemplate(input, { mode: 'harmony' })
+ expect(warnFn).not.toHaveBeenCalled()
+ expect(errorFn).not.toHaveBeenCalled()
+ })
+
+ it('should handle conditional compilation attributes correctly', function () {
+ // 测试 wx to ali 场景下的条件编译属性
+ const input1 = 'content'
+ compileTemplate(input1, { srcMode: 'wx', mode: 'ali' })
+ expect(warnFn).not.toHaveBeenCalled() // 应该不警告,因为目标是ali模式
+
+ const input2 = 'content'
+ compileTemplate(input2, { srcMode: 'wx', mode: 'ali' })
+ expect(warnFn).not.toHaveBeenCalled() // 应该不警告,因为目标是ali模式
+ })
+
+ it('should warn when using a: prefix in web mode', function () {
+ // web 模式应该使用 v- 前缀,使用 a: 会警告
+ const input = 'content'
+ compileTemplate(input, { mode: 'web' })
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "web", but used "a:class". Did you mean "v-class"?')
+ )
+ })
+
+ it('should warn when using s- prefix in web mode', function () {
+ // web 模式应该使用 v- 前缀,使用 s- 会警告
+ const input = 'content'
+ compileTemplate(input, { mode: 'web' })
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "web", but used "s-if". Did you mean "v-if"?')
+ )
+ })
+
+ it('should not warn when using v- prefix in web mode', function () {
+ // web 模式使用 v- 前缀不应该警告
+ const input = '
content
'
+ compileTemplate(input, { mode: 'web' })
+ expect(warnFn).not.toHaveBeenCalled()
+ })
+
+ it('should warn when using v- prefix in miniprogram mode', function () {
+ // 小程序模式使用 v- 前缀应该警告
+ const input = 'content'
+ compileTemplate(input, { mode: 'wx' })
+ expect(warnFn).toHaveBeenCalledWith(
+ expect.stringContaining('Your target mode is "wx", but used "v-if". Did you mean "wx:if"?')
+ )
+ })
+})