feat:转账插件

This commit is contained in:
2026-07-23 14:31:06 +08:00
parent 4916546322
commit d5a6a16ea5
560 changed files with 71777 additions and 44216 deletions
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2024-present, Skiyee (Sky)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+151
View File
@@ -0,0 +1,151 @@
import { dirname, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import process from 'node:process'
import { existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'
import { createFilter, normalizePath } from 'vite'
import { transformNvuePage, transformPage } from './page.js'
import { rebuildUpApp, registerUpApp } from './root.js'
import { loadPagesJson, normalizePlatformPath, toArray } from './utils.js'
const rootLibPath = normalizePath(dirname(fileURLToPath(import.meta.url)))
export default function UniUpRoot(options = {}) {
const rootOptions = {
enabledVirtualHost: false,
enabledGlobalRef: false,
rootFileName: 'App.up',
autoCreateRootFile: true,
excludePages: [],
...options,
}
const rootFileName = String(rootOptions.rootFileName || 'App.up').replace(/\.vue$/i, '')
const detectProjectRoot = () => {
const cwd = normalizePath(process.env.INIT_CWD || process.cwd())
const envInputDir = process.env.UNI_INPUT_DIR ? normalizePath(process.env.UNI_INPUT_DIR) : ''
const cliRootPath = normalizePath(resolve(cwd, 'src'))
const hbuilderRootPath = cwd
const isPageRoot = (path) => existsSync(resolve(path, 'pages.json'))
if (envInputDir && isPageRoot(envInputDir)) {
const projectType = envInputDir.endsWith('/src') ? 'cli' : 'hbuilder'
return { rootPath: envInputDir, projectType }
}
if (isPageRoot(cliRootPath)) {
return { rootPath: cliRootPath, projectType: 'cli' }
}
if (isPageRoot(hbuilderRootPath)) {
return { rootPath: hbuilderRootPath, projectType: 'hbuilder' }
}
return {
rootPath: envInputDir || cliRootPath,
projectType: envInputDir && !envInputDir.endsWith('/src') ? 'hbuilder' : 'cli',
}
}
const projectInfo = detectProjectRoot()
const rootPath = normalizePath(projectInfo.rootPath)
const appUpPath = normalizePath(resolve(rootPath, `${rootFileName}.vue`))
const rootToastHostPath = normalizePath(resolve(rootLibPath, 'root-toast-host.vue'))
const nvueRootPath = normalizePath(resolve(rootPath, 'uni_modules/uview-plus/libs/root/nvue-root.vue'))
const themeRuntimePath = normalizePath(resolve(rootPath, 'uni_modules/uview-plus/libs/theme/runtime.js'))
const pagesPath = normalizePath(resolve(rootPath, 'pages.json'))
const excludedPaths = toArray(rootOptions.excludePages)
.filter(Boolean)
.map(path => normalizePath(resolve(rootPath, path)))
const mainFiles = [
normalizePath(resolve(rootPath, 'main.ts')),
normalizePath(resolve(rootPath, 'main.js')),
]
const getRelativeImportPath = (fromFile, toFile) => {
let importPath = normalizePath(relative(dirname(fromFile), toFile))
if (!importPath.startsWith('.')) {
importPath = `./${importPath}`
}
return importPath
}
let pagesJson = []
let pagesJsonMtimeMs = 0
let hasPlatformPlugin = false
const ensureRootFile = () => {
if (!rootOptions.autoCreateRootFile) return
if (existsSync(appUpPath)) return
const defaultRootSfc = `<template>
\t<UpRootView />
</template>
`
mkdirSync(dirname(appUpPath), { recursive: true })
writeFileSync(appUpPath, defaultRootSfc, 'utf-8')
}
const refreshPagesJson = () => {
if (!existsSync(pagesPath)) return
const mtimeMs = statSync(pagesPath).mtimeMs
if (mtimeMs === pagesJsonMtimeMs && pagesJson.length) return
pagesJson = loadPagesJson(pagesPath, rootPath)
pagesJsonMtimeMs = mtimeMs
}
return {
name: 'vite-plugin-uni-up-root',
enforce: 'pre',
configResolved({ plugins }) {
hasPlatformPlugin = plugins.some(v => v.name === 'vite-plugin-uni-platform')
},
buildStart() {
ensureRootFile()
refreshPagesJson()
},
async transform(code, id) {
let ms = null
const isSfcBlock = id.includes('?')
const cleanId = normalizePath(id.split('?')[0])
const filterMain = createFilter(mainFiles)
if (filterMain(cleanId)) {
ms = await registerUpApp(code, rootFileName, getRelativeImportPath(cleanId, rootToastHostPath))
}
const filterUpRoot = createFilter(appUpPath)
if (filterUpRoot(cleanId)) {
ms = await rebuildUpApp(code, rootOptions.enabledVirtualHost)
}
refreshPagesJson()
const pageId = hasPlatformPlugin ? normalizePlatformPath(cleanId) : cleanId
const filterPage = createFilter(pagesJson, excludedPaths)
if (!isSfcBlock && filterPage(pageId)) {
if (cleanId.endsWith('.nvue')) {
ms = await transformNvuePage(
code,
getRelativeImportPath(cleanId, nvueRootPath),
getRelativeImportPath(cleanId, themeRuntimePath),
rootOptions.enabledGlobalRef
)
} else {
ms = await transformPage(code, rootOptions.enabledGlobalRef)
}
}
if (ms) {
return {
code: ms.toString(),
map: ms.generateMap({ hires: true }),
}
}
return null
},
}
}
@@ -0,0 +1,72 @@
<template>
<view class="up-nvue-root" :style="rootStyle">
<slot />
</view>
</template>
<script>
import { applyNativeThemeUI, getThemePageStyle, getThemeVarsForStyle, syncThemeRuntimeFromStorage } from '../theme/runtime.js'
export default {
name: 'UpNvueRoot',
data() {
return {
rootStyle: {
...getThemeVarsForStyle(),
...getThemePageStyle()
},
minPageHeight: '0px'
}
},
created() {
this.syncPageHeight()
this.refreshRootStyle()
this.themeChangeHandler = () => {
this.refreshRootStyle()
}
if (typeof uni !== 'undefined' && typeof uni.$on === 'function') {
uni.$on('uThemeChange', this.themeChangeHandler)
}
},
mounted() {
this.refreshRootStyle()
},
beforeUnmount() {
if (this.themeChangeHandler && typeof uni !== 'undefined' && typeof uni.$off === 'function') {
uni.$off('uThemeChange', this.themeChangeHandler)
}
this.themeChangeHandler = null
},
methods: {
syncPageHeight() {
let windowHeight = 0
try {
if (typeof uni !== 'undefined' && typeof uni.getWindowInfo === 'function') {
windowHeight = Number((uni.getWindowInfo() || {}).windowHeight || 0)
} else if (typeof uni !== 'undefined' && typeof uni.getSystemInfoSync === 'function') {
windowHeight = Number((uni.getSystemInfoSync() || {}).windowHeight || 0)
}
} catch (e) {
windowHeight = 0
}
this.minPageHeight = `${windowHeight || 0}px`
},
refreshRootStyle() {
syncThemeRuntimeFromStorage()
this.rootStyle = {
...getThemeVarsForStyle(),
...getThemePageStyle(),
minHeight: this.minPageHeight,
width: '750rpx'
}
applyNativeThemeUI()
}
}
}
</script>
<style>
.up-nvue-root {
width: 750rpx;
}
</style>
+269
View File
@@ -0,0 +1,269 @@
import { MagicString } from 'vue/compiler-sfc'
import { findNode, parseSFC } from './utils.js'
function getPageRootRefSource(sfc, enabledGlobalRef = false) {
const pageTempAttrs = sfc.template?.attrs
if (pageTempAttrs && pageTempAttrs.root) {
return `ref="${pageTempAttrs.root}"`
}
return enabledGlobalRef ? 'ref="uniUpRoot"' : ''
}
function ensureNvueNavigationBar(pageMetaSource) {
const navigationBarSource = '<navigation-bar :front-color="upThemeNavFrontColor" :background-color="upThemeNavBgColor" />'
if (!pageMetaSource) return `<page-meta>${navigationBarSource}</page-meta>`
if (pageMetaSource.indexOf('navigation-bar') > -1 || pageMetaSource.indexOf('NavigationBar') > -1) {
return pageMetaSource
}
return pageMetaSource.replace(/<page-meta([^>]*)>/i, `<page-meta$1>${navigationBarSource}`)
}
function wrapTemplate(ms, sfc, tagName, rootRefSource = '', withNvueNavigationBar = false) {
const pageTempStart = sfc.template?.loc.start.offset
const pageTempEnd = sfc.template?.loc.end.offset
let pageMetaSource = ''
const pageMetaNode = findNode(sfc, 'PageMeta')
if (pageMetaNode) {
pageMetaSource = pageMetaNode.loc.source
const metaTempStart = pageMetaNode.loc.start.offset
const metaTempEnd = pageMetaNode.loc.end.offset
ms.remove(metaTempStart, metaTempEnd)
}
if (withNvueNavigationBar) {
pageMetaSource = ensureNvueNavigationBar(pageMetaSource)
}
if (pageTempStart != null && pageTempEnd != null) {
ms.appendLeft(pageTempStart, `\n${pageMetaSource}\n<${tagName} ${rootRefSource}>`)
ms.appendRight(pageTempEnd, `\n</${tagName}>\n`)
}
}
export async function transformPage(code, enabledGlobalRef = false) {
const sfc = await parseSFC(code)
const ms = new MagicString(code)
wrapTemplate(ms, sfc, 'global-up-root', getPageRootRefSource(sfc, enabledGlobalRef))
return ms
}
function ensureOptionsComponent(ms, scriptContent) {
if (scriptContent.includes('UpNvueRoot')) return
const componentsMatch = scriptContent.match(/components\s*:\s*\{/)
if (componentsMatch) {
ms.appendLeft(componentsMatch.index + componentsMatch[0].length, ' UpNvueRoot,')
return
}
const exportDefaultMatch = scriptContent.match(/export\s+default\s+(?:defineComponent\s*\()?\s*\{/)
if (exportDefaultMatch) {
ms.appendLeft(
exportDefaultMatch.index + exportDefaultMatch[0].length,
'\n components: { UpNvueRoot },'
)
}
}
function ensureOptionsThemeMixin(ms, scriptContent) {
if (scriptContent.includes('__upNvueThemeMixin')) return
const mixinsMatch = scriptContent.match(/mixins\s*:\s*\[/)
if (mixinsMatch) {
ms.appendLeft(mixinsMatch.index + mixinsMatch[0].length, ' __upNvueThemeMixin,')
return
}
const exportDefaultMatch = scriptContent.match(/export\s+default\s+(?:defineComponent\s*\()?\s*\{/)
if (exportDefaultMatch) {
ms.appendLeft(
exportDefaultMatch.index + exportDefaultMatch[0].length,
'\n mixins: [__upNvueThemeMixin],'
)
}
}
function getScriptSetupImports(componentImportPath, runtimeImportPath) {
return `import UpNvueRoot from '${componentImportPath}'
import { computed as __upComputed, onBeforeUnmount as __upOnBeforeUnmount, onMounted as __upOnMounted, ref as __upRef } from 'vue'
import { onShow as __upOnShow } from '@dcloudio/uni-app'
import {
applyNativeThemeUI as __upApplyNativeThemeUI,
applyNativeThemeUIDeferred as __upApplyNativeThemeUIDeferred,
getThemeCardStyle as __upGetThemeCardStyle,
getThemeIsDark as __upGetThemeIsDark,
getThemePageStyle as __upGetThemePageStyle,
getThemeVar as __upThemeVar,
syncThemeRuntimeFromStorage as __upSyncThemeRuntimeFromStorage
} from '${runtimeImportPath}'
const __upThemeVersion = __upRef(0)
let __upThemeChangeHandler = null
const __upReadThemeVersion = () => Number((typeof uni !== 'undefined' && uni.$u && uni.$u.theme && uni.$u.theme.version) || 0)
const __upSyncThemeVersion = (payload = {}) => {
const version = Number(payload.version || __upReadThemeVersion() || 0)
__upThemeVersion.value = version || Number(__upThemeVersion.value || 0) + 1
}
__upOnMounted(() => {
__upSyncThemeRuntimeFromStorage()
__upSyncThemeVersion()
__upApplyNativeThemeUI()
if (typeof uni !== 'undefined' && typeof uni.$on === 'function') {
__upThemeChangeHandler = (payload = {}) => {
__upSyncThemeRuntimeFromStorage()
__upSyncThemeVersion(payload)
__upApplyNativeThemeUI()
}
uni.$on('uThemeChange', __upThemeChangeHandler)
}
})
__upOnShow(() => {
__upSyncThemeRuntimeFromStorage()
__upSyncThemeVersion()
__upApplyNativeThemeUIDeferred()
})
__upOnBeforeUnmount(() => {
if (typeof uni !== 'undefined' && typeof uni.$off === 'function' && __upThemeChangeHandler) {
uni.$off('uThemeChange', __upThemeChangeHandler)
__upThemeChangeHandler = null
}
})
const upThemeVar = (varName, fallbackColor) => {
__upThemeVersion.value
return __upThemeVar(varName, fallbackColor)
}
const upThemeCardStyle = __upComputed(() => {
__upThemeVersion.value
return __upGetThemeCardStyle()
})
const upThemePageStyle = __upComputed(() => {
__upThemeVersion.value
return __upGetThemePageStyle()
})
const upThemeIsDark = __upComputed(() => {
__upThemeVersion.value
return __upGetThemeIsDark()
})
const upThemeNavFrontColor = __upComputed(() => {
__upThemeVersion.value
return __upGetThemeIsDark() ? '#ffffff' : '#000000'
})
const upThemeNavBgColor = __upComputed(() => {
__upThemeVersion.value
return __upThemeVar('--up-navbar-bg-color', __upGetThemeIsDark() ? '#1c1c1e' : '#ffffff')
})
`
}
function getOptionsImports(componentImportPath, runtimeImportPath) {
return `import UpNvueRoot from '${componentImportPath}'
import {
applyNativeThemeUI as __upApplyNativeThemeUI,
applyNativeThemeUIDeferred as __upApplyNativeThemeUIDeferred,
getThemeCardStyle as __upGetThemeCardStyle,
getThemeIsDark as __upGetThemeIsDark,
getThemePageStyle as __upGetThemePageStyle,
getThemeVar as __upThemeVar,
syncThemeRuntimeFromStorage as __upSyncThemeRuntimeFromStorage
} from '${runtimeImportPath}'
const __upNvueThemeMixin = {
data() {
return {
__upNvueThemeVersion: 0,
__upNvueThemeChangeHandler: null
}
},
created() {
__upSyncThemeRuntimeFromStorage()
this.__upSyncNvueThemeVersion()
__upApplyNativeThemeUI()
if (typeof uni !== 'undefined' && typeof uni.$on === 'function') {
this.__upNvueThemeChangeHandler = (payload = {}) => {
__upSyncThemeRuntimeFromStorage()
this.__upSyncNvueThemeVersion(payload)
__upApplyNativeThemeUI()
}
uni.$on('uThemeChange', this.__upNvueThemeChangeHandler)
}
},
onShow() {
__upSyncThemeRuntimeFromStorage()
this.__upSyncNvueThemeVersion()
__upApplyNativeThemeUIDeferred()
},
beforeUnmount() {
if (typeof uni !== 'undefined' && typeof uni.$off === 'function' && this.__upNvueThemeChangeHandler) {
uni.$off('uThemeChange', this.__upNvueThemeChangeHandler)
this.__upNvueThemeChangeHandler = null
}
},
computed: {
upThemeCardStyle() {
this.__upNvueThemeVersion
return __upGetThemeCardStyle()
},
upThemePageStyle() {
this.__upNvueThemeVersion
return __upGetThemePageStyle()
},
upThemeIsDark() {
this.__upNvueThemeVersion
return __upGetThemeIsDark()
},
upThemeNavFrontColor() {
this.__upNvueThemeVersion
return __upGetThemeIsDark() ? '#ffffff' : '#000000'
},
upThemeNavBgColor() {
this.__upNvueThemeVersion
return __upThemeVar('--up-navbar-bg-color', __upGetThemeIsDark() ? '#1c1c1e' : '#ffffff')
}
},
methods: {
__upReadNvueThemeVersion() {
return Number((typeof uni !== 'undefined' && uni.$u && uni.$u.theme && uni.$u.theme.version) || 0)
},
__upSyncNvueThemeVersion(payload = {}) {
const version = Number(payload.version || this.__upReadNvueThemeVersion() || 0)
this.__upNvueThemeVersion = version || Number(this.__upNvueThemeVersion || 0) + 1
},
upThemeVar(varName, fallbackColor) {
this.__upNvueThemeVersion
return __upThemeVar(varName, fallbackColor)
}
}
}
`
}
export async function transformNvuePage(
code,
componentImportPath,
runtimeImportPath,
enabledGlobalRef = false
) {
const sfc = await parseSFC(code)
const ms = new MagicString(code)
wrapTemplate(ms, sfc, 'UpNvueRoot', getPageRootRefSource(sfc, enabledGlobalRef), true)
if (sfc.scriptSetup) {
ms.appendLeft(sfc.scriptSetup.loc.start.offset, getScriptSetupImports(componentImportPath, runtimeImportPath))
} else if (sfc.script) {
ms.appendLeft(sfc.script.loc.start.offset, getOptionsImports(componentImportPath, runtimeImportPath))
ensureOptionsComponent(ms, sfc.script.content)
ensureOptionsThemeMixin(ms, sfc.script.content)
} else {
ms.append(
`\n<script>\n${getOptionsImports(componentImportPath, runtimeImportPath)}export default {\n components: { UpNvueRoot },\n mixins: [__upNvueThemeMixin]\n}\n</script>\n`
)
}
return ms
}
@@ -0,0 +1,31 @@
<template>
<up-toast ref="upGlobalToastRef" />
<up-notify ref="upGlobalNotifyRef" />
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref } from 'vue'
const upGlobalToastRef = ref(null)
const upGlobalNotifyRef = ref(null)
onMounted(() => {
if (typeof uni === 'undefined' || !uni.$u) return
if (typeof uni.$u.setRootToastRef === 'function') {
uni.$u.setRootToastRef(upGlobalToastRef.value)
}
if (typeof uni.$u.setRootNotifyRef === 'function') {
uni.$u.setRootNotifyRef(upGlobalNotifyRef.value)
}
})
onBeforeUnmount(() => {
if (typeof uni === 'undefined' || !uni.$u) return
if (typeof uni.$u.setRootToastRef === 'function') {
uni.$u.setRootToastRef(null)
}
if (typeof uni.$u.setRootNotifyRef === 'function') {
uni.$u.setRootNotifyRef(null)
}
})
</script>
+44
View File
@@ -0,0 +1,44 @@
import { MagicString } from 'vue/compiler-sfc'
import { parseSFC } from './utils.js'
export async function registerUpApp(
code,
fileName = 'App.up',
rootToastHostPath = './uni_modules/uview-plus/libs/root/root-toast-host.vue'
) {
const ms = new MagicString(code)
const importCode = `import GlobalUpRoot from "./${fileName}.vue";
import UpRootToastHost from "${rootToastHostPath}";`
const vueUseComponentCode = 'app.component("global-up-root", GlobalUpRoot);\napp.component("ku-root-toast-host", UpRootToastHost);'
ms.prepend(`${importCode}\n`).replace(
/(createApp[\s\S]*?)(return\s\{\s*app)/,
`$1${vueUseComponentCode}\n$2`,
)
return ms
}
export async function rebuildUpApp(code, enabledVirtualHost = false) {
const ms = new MagicString(code)
const rootTagNameRE = /<(UpRootView|up-root-view)(?:\s*\/>|><\/\1>)/
ms.replace(rootTagNameRE, '<slot />\n <ku-root-toast-host />')
if (enabledVirtualHost) {
const sfc = await parseSFC(code)
if (sfc.script) {
return ms
}
const langType = sfc.scriptSetup?.lang
ms.append(`<script ${langType ? `lang="${langType}"` : ''}>
export default {
options: {
virtualHost: true,
}
}\n</script>`)
}
return ms
}
@@ -0,0 +1,11 @@
// Compatibility shim for stale incremental build caches.
// New root integration no longer depends on this file.
export function createGlobalUpRoot(AppRootComponent) {
return AppRootComponent
}
export default {
createGlobalUpRoot,
}
+115
View File
@@ -0,0 +1,115 @@
import { existsSync, readFileSync } from 'node:fs'
import { extname, join } from 'node:path'
import process from 'node:process'
import { parse as VueParser } from 'vue/compiler-sfc'
import { normalizePath } from 'vite'
function stripJsonComments(jsonText) {
return jsonText
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/(^|[^:\\])\/\/.*$/gm, '$1')
}
function parseJsonc(jsonText) {
const withoutComments = stripJsonComments(jsonText)
const withoutTrailingComma = withoutComments.replace(/,\s*([}\]])/g, '$1')
return JSON.parse(withoutTrailingComma)
}
export async function parseSFC(code) {
try {
return VueParser(code, { pad: 'space' }).descriptor
} catch {
throw new Error('[@up-root] Vue version must support compiler-sfc parser.')
}
}
const PAGE_FILE_EXTS = ['.vue', '.nvue', '.uvue']
export function formatPagePath(root, path) {
const joinedPath = join(root, path)
const pathExt = extname(joinedPath)
if (pathExt) {
return normalizePath(joinedPath)
}
const pageFilePath = PAGE_FILE_EXTS
.map(fileExt => `${joinedPath}${fileExt}`)
.find(filePath => existsSync(filePath))
return normalizePath(pageFilePath || `${joinedPath}.vue`)
}
export function loadPagesJson(path, rootPath) {
const pagesJsonRaw = readFileSync(path, 'utf-8')
const parsed = parseJsonc(pagesJsonRaw) || {}
const pages = parsed.pages || []
const subPackages = parsed.subPackages || []
return [
...pages.map((page) => formatPagePath(rootPath, page.path)),
...subPackages
.map(({ pages: subPages = [], root = '' }) => {
return subPages.map((page) => formatPagePath(join(rootPath, root), page.path))
})
.flat(),
]
}
export function toKebabCase(str) {
return str
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/[_\s]+/g, '-')
.toLowerCase()
}
export function toPascalCase(str) {
return str
.replace(/(^\w|-+\w)/g, (match) => match.toUpperCase().replace(/-/g, ''))
}
export function findNode(sfc, rawTagName) {
const templateSource = sfc.template?.content
if (!templateSource) return
let tagName = ''
if (templateSource.includes(`<${toKebabCase(rawTagName)}`)) {
tagName = toKebabCase(rawTagName)
} else if (templateSource.includes(`<${toPascalCase(rawTagName)}`)) {
tagName = toPascalCase(rawTagName)
}
if (!tagName) return
const nodeAst = sfc.template?.ast
if (!nodeAst) return
const traverse = (nodes) => {
for (const node of nodes) {
if (node.type === 1) {
if (node.tag === tagName) return node
if (node.children?.length) {
const found = traverse(node.children)
if (found) return found
}
}
}
}
return traverse(nodeAst.children)
}
const platform = process.env.UNI_PLATFORM
export function normalizePlatformPath(id) {
const idExt = extname(id)
if (idExt !== '.vue') return id
if (!id.includes(`.${platform}.`)) return id
return id.replace(`.${platform}.`, '.')
}
export function toArray(value) {
if (!value) return []
return Array.isArray(value) ? value : [value]
}