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"?') + ) + }) +})