Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs-vitepress/guide/basic/start.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ mpx-cli-service build --targets=wx,ali
| QQ | qq |
| 头条 | tt |
| 浏览器 | web |
| 快应用 | qa |
| 安卓 | android |
| iOS | ios |
| 鸿蒙 | harmony |
Expand Down
81 changes: 80 additions & 1 deletion packages/webpack-plugin/lib/template-compiler/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -120,6 +120,8 @@ const rulesResultMap = new Map()
let usingComponents = []
let usingComponentsInfo = {}
let componentGenerics = {}
// 跨平台语法检测的配置,在模块加载时初始化一次
let crossPlatformConfig = null

function updateForScopesMap () {
forScopesMap = {}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -2880,6 +2956,9 @@ function processElement (el, root, options, meta) {

processDuplicateAttrsList(el)

// 检测跨平台语法使用情况并给出警告
processCrossPlatformSyntaxWarning(el)

processInjectWxs(el, meta, options)

const transAli = mode === 'ali' && srcMode === 'wx'
Expand Down
7 changes: 6 additions & 1 deletion packages/webpack-plugin/lib/utils/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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 = '<view wx:class="{{someClass}}">content</view>'
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 = '<view a:class="{{someClass}}">content</view>'
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 = '<view s-if="{{condition}}">content</view>'
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 = '<view qq:for="{{items}}">content</view>'
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 = '<view tt:style="{{dynamicStyle}}">content</view>'
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 = '<view dd:show="{{visible}}">content</view>'
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 = '<view wx:class="{{someClass}}">content</view>'
compileTemplate(input1, { mode: 'wx' })
expect(warnFn).not.toHaveBeenCalled()

const input2 = '<view a:class="{{someClass}}">content</view>'
compileTemplate(input2, { mode: 'ali' })
expect(warnFn).not.toHaveBeenCalled()

const input3 = '<view s-if="{{condition}}">content</view>'
compileTemplate(input3, { mode: 'swan' })
expect(warnFn).not.toHaveBeenCalled()

const input4 = '<view qq:for="{{items}}">content</view>'
compileTemplate(input4, { mode: 'qq' })
expect(warnFn).not.toHaveBeenCalled()

const input5 = '<view tt:style="{{dynamicStyle}}">content</view>'
compileTemplate(input5, { mode: 'tt' })
expect(warnFn).not.toHaveBeenCalled()
})

it('should not warn for regular attributes without platform prefixes', function () {
const input = '<view class="normal" style="color: red;" id="test">content</view>'
compileTemplate(input, { mode: 'ali' })
expect(warnFn).not.toHaveBeenCalled()
})

it('should warn for cross-platform attributes without auto-conversion', function () {
// 只检测没有自动转换规则的属性
const input = '<view s-if="{{condition}}" qq:for="{{items}}">content</view>'
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 = `
<view s-if="{{condition}}">
<text s-class="{{textClass}}">Hello</text>
</view>
`
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 = '<view jd:ref="myRef">content</view>'
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 = '<view qa:key="{{item.id}}">content</view>'
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 = '<view a:class="{{someClass}}">content</view>'

// 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 = '<view wx:class="{{someClass}}">content</view>'

// 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 = '<alicom@ali a:if="{{show}}">content</alicom>'
compileTemplate(input1, { srcMode: 'wx', mode: 'ali' })
expect(warnFn).not.toHaveBeenCalled() // 应该不警告,因为目标是ali模式

const input2 = '<com a:if@ali="{{show}}">content</com>'
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 = '<view a:class="someClass">content</view>'
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 = '<view s-if="condition">content</view>'
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 = '<div v-if="condition">content</div>'
compileTemplate(input, { mode: 'web' })
expect(warnFn).not.toHaveBeenCalled()
})

it('should warn when using v- prefix in miniprogram mode', function () {
// 小程序模式使用 v- 前缀应该警告
const input = '<view v-if="condition">content</view>'
compileTemplate(input, { mode: 'wx' })
expect(warnFn).toHaveBeenCalledWith(
expect.stringContaining('Your target mode is "wx", but used "v-if". Did you mean "wx:if"?')
)
})
})