提交 ba5c9717 编写于 作者: fxy060608's avatar fxy060608

wip(uts): compiler

上级 b1daddf4
...@@ -37,6 +37,7 @@ declare namespace NodeJS { ...@@ -37,6 +37,7 @@ declare namespace NodeJS {
UNI_APP_CHANGED_PAGES: string UNI_APP_CHANGED_PAGES: string
VUE_APP_DARK_MODE: 'true' | 'false' VUE_APP_DARK_MODE: 'true' | 'false'
HX_USE_BASE_TYPE?: 'standard' | 'custom' HX_USE_BASE_TYPE?: 'standard' | 'custom'
HX_DEPENDENCIES_DIR?: string
__VUE_PROD_DEVTOOLS__?: 'true' __VUE_PROD_DEVTOOLS__?: 'true'
__VUE_DEVTOOLS_HOST__: string __VUE_DEVTOOLS_HOST__: string
......
...@@ -2,10 +2,9 @@ import { resolve } from 'path' ...@@ -2,10 +2,9 @@ import { resolve } from 'path'
import { import {
checkManifest, checkManifest,
resolveManifestJson, resolveManifestJson,
resolvePluginAndroidFiles, resolvePluginFiles,
resolvePluginIOSFiles,
} from '../src/manifest/manifest' } from '../src/manifest/manifest'
import { checkKotlinCompile } from '../src/manifest/index' import { checkKotlinCompile, checkSwiftCompile } from '../src/manifest/index'
const pluginModuleDir = resolve(__dirname, 'examples/uts/uni_modules/test-uts') const pluginModuleDir = resolve(__dirname, 'examples/uts/uni_modules/test-uts')
const pluginDir = resolve(__dirname, 'examples/uts/utssdk/test-uts') const pluginDir = resolve(__dirname, 'examples/uts/utssdk/test-uts')
...@@ -36,13 +35,15 @@ const pluginOptions = { ...@@ -36,13 +35,15 @@ const pluginOptions = {
describe('manifest', () => { describe('manifest', () => {
test('resolve android files', async () => { test('resolve android files', async () => {
expect(await resolvePluginAndroidFiles(pluginDir, false)).toEqual([ expect(await resolvePluginFiles('app-android', pluginDir, false)).toEqual([
'index.uts', 'index.uts',
'package.json', 'package.json',
'common/utils.uts', 'common/utils.uts',
'common/test/test.uts', 'common/test/test.uts',
]) ])
expect(await resolvePluginAndroidFiles(pluginModuleDir, true)).toEqual([ expect(
await resolvePluginFiles('app-android', pluginModuleDir, true)
).toEqual([
'package.json', 'package.json',
'utssdk/common/utils.uts', 'utssdk/common/utils.uts',
'utssdk/app-android/index.uts', 'utssdk/app-android/index.uts',
...@@ -50,13 +51,13 @@ describe('manifest', () => { ...@@ -50,13 +51,13 @@ describe('manifest', () => {
]) ])
}) })
test('resolve ios files', async () => { test('resolve ios files', async () => {
expect(await resolvePluginIOSFiles(pluginDir, false)).toEqual([ expect(await resolvePluginFiles('app-ios', pluginDir, false)).toEqual([
'index.uts', 'index.uts',
'package.json', 'package.json',
'common/utils.uts', 'common/utils.uts',
'common/test/test.uts', 'common/test/test.uts',
]) ])
expect(await resolvePluginIOSFiles(pluginModuleDir, true)).toEqual([ expect(await resolvePluginFiles('app-ios', pluginModuleDir, true)).toEqual([
'package.json', 'package.json',
'utssdk/common/utils.uts', 'utssdk/common/utils.uts',
'utssdk/app-ios/index.uts', 'utssdk/app-ios/index.uts',
...@@ -99,17 +100,24 @@ describe('manifest', () => { ...@@ -99,17 +100,24 @@ describe('manifest', () => {
).toBe(filename) ).toBe(filename)
}) })
test('gen android manifest', async () => { test('gen android manifest', async () => {
expect( const res = await checkKotlinCompile('standard', pluginModuleOptions)
await (
await checkKotlinCompile('standard', pluginModuleOptions)
).expired
).toBe(true)
const res = await checkKotlinCompile('standard', pluginOptions)
expect(res.expired).toBe(false) expect(res.expired).toBe(false)
expect(res.cacheFile).toContain('classes.dex') expect(res.files.length).toBe(4)
expect(res.tips).toBeTruthy()
const res1 = await checkKotlinCompile('standard', pluginOptions)
expect(res1.expired).toBe(false)
expect(res1.files.length).toBe(4)
expect(res1.tips).toBe('')
}) })
test('gen ios manifest', async () => { test('gen ios manifest', async () => {
// console.log(await checkCompile('standard', pluginModuleOptions)) const res = await checkSwiftCompile('standard', pluginModuleOptions)
expect(res.expired).toBe(false)
expect(res.files.length).toBe(4)
expect(res.tips).toBe('')
const res1 = await checkSwiftCompile('standard', pluginOptions)
expect(res1.expired).toBe(false)
expect(res1.files.length).toBe(4)
expect(res1.tips).toBe('')
}) })
}) })
import { isArray } from '@vue/shared' import { isArray } from '@vue/shared'
import { basename, join, relative } from 'path'
import { copySync, existsSync } from 'fs-extra'
import { runKotlinProd, runKotlinDev } from './kotlin' import { runKotlinProd, runKotlinDev, resolveAndroidDepFiles } from './kotlin'
import { runSwiftProd, runSwiftDev } from './swift' import { runSwiftProd, runSwiftDev, resolveIOSDepFiles } from './swift'
import { genProxyCode, resolvePlatformIndex, resolveRootIndex } from './code' import { genProxyCode, resolvePlatformIndex, resolveRootIndex } from './code'
import { ERR_MSG_PLACEHOLDER, resolvePackage } from './utils' import { ERR_MSG_PLACEHOLDER, resolvePackage } from './utils'
...@@ -12,6 +14,7 @@ import { ...@@ -12,6 +14,7 @@ import {
generateCodeFrameWithKotlinStacktrace, generateCodeFrameWithKotlinStacktrace,
generateCodeFrameWithSwiftStacktrace, generateCodeFrameWithSwiftStacktrace,
} from './legacy' } from './legacy'
import { checkCompile, genManifestFile, initCheckOptionsEnv } from './manifest'
export const sourcemap = { export const sourcemap = {
generateCodeFrameWithKotlinStacktrace, generateCodeFrameWithKotlinStacktrace,
...@@ -31,41 +34,59 @@ function compileErrMsg(id: string) { ...@@ -31,41 +34,59 @@ function compileErrMsg(id: string) {
return `uts插件[${id}]编译失败,无法使用` return `uts插件[${id}]编译失败,无法使用`
} }
export async function compile(module: string) { export async function compile(pluginDir: string) {
const pkg = resolvePackage(module) const pkg = resolvePackage(pluginDir)
if (!pkg) { if (!pkg) {
return return
} }
const cacheDir = process.env.HX_DEPENDENCIES_DIR
const inputDir = process.env.UNI_INPUT_DIR
const outputDir = process.env.UNI_OUTPUT_DIR
const utsPlatform = process.env.UNI_UTS_PLATFORM
const pluginRelativeDir = relative(inputDir, pluginDir)
const env = initCheckOptionsEnv()
const deps: string[] = [] const deps: string[] = []
const code = await genProxyCode(module, pkg) const code = await genProxyCode(pluginDir, pkg)
let errMsg = '' let errMsg = ''
if (process.env.NODE_ENV !== 'development') { if (process.env.NODE_ENV !== 'development') {
// 生产模式 支持同时生成 android 和 ios 的 uts 插件 // 生产模式 支持同时生成 android 和 ios 的 uts 插件
if ( if (utsPlatform === 'app-android' || utsPlatform === 'app') {
process.env.UNI_UTS_PLATFORM === 'app-android' ||
process.env.UNI_UTS_PLATFORM === 'app'
) {
const filename = const filename =
resolvePlatformIndex('app-android', module, pkg) || resolvePlatformIndex('app-android', pluginDir, pkg) ||
resolveRootIndex(module, pkg) resolveRootIndex(pluginDir, pkg)
if (filename) { if (filename) {
await getCompiler('kotlin').runProd(filename) await getCompiler('kotlin').runProd(filename)
if (cacheDir) {
genManifestFile('app-android', {
pluginDir,
env,
cacheDir,
pluginRelativeDir,
is_uni_modules: pkg.is_uni_modules,
})
}
} }
} }
if ( if (utsPlatform === 'app-ios' || utsPlatform === 'app') {
process.env.UNI_UTS_PLATFORM === 'app-ios' ||
process.env.UNI_UTS_PLATFORM === 'app'
) {
const filename = const filename =
resolvePlatformIndex('app-ios', module, pkg) || resolvePlatformIndex('app-ios', pluginDir, pkg) ||
resolveRootIndex(module, pkg) resolveRootIndex(pluginDir, pkg)
if (filename) { if (filename) {
await getCompiler('swift').runProd(filename) await getCompiler('swift').runProd(filename)
if (cacheDir) {
genManifestFile('app-ios', {
pluginDir,
env,
cacheDir,
pluginRelativeDir,
is_uni_modules: pkg.is_uni_modules,
})
}
} }
} }
} else { } else {
// iOS windows 平台,标准基座不编译 // iOS windows 平台,标准基座不编译
if (process.env.UNI_UTS_PLATFORM === 'app-ios') { if (utsPlatform === 'app-ios') {
if (isWindows) { if (isWindows) {
process.env.UNI_UTS_TIPS = `iOS手机在windows上真机运行时uts插件代码修改需提交云端打包自定义基座才能生效` process.env.UNI_UTS_TIPS = `iOS手机在windows上真机运行时uts插件代码修改需提交云端打包自定义基座才能生效`
return { return {
...@@ -81,25 +102,66 @@ export async function compile(module: string) { ...@@ -81,25 +102,66 @@ export async function compile(module: string) {
} }
} }
} }
if ( if (utsPlatform === 'app-android' || utsPlatform === 'app-ios') {
process.env.UNI_UTS_PLATFORM === 'app-android' ||
process.env.UNI_UTS_PLATFORM === 'app-ios'
) {
// dev 模式 // dev 模式
if (cacheDir) {
// 检查缓存
let start = Date.now()
const res = await checkCompile(
utsPlatform,
process.env.HX_USE_BASE_TYPE,
{
id: pkg.id,
env,
cacheDir,
outputDir,
pluginDir,
pluginRelativeDir,
is_uni_modules: pkg.is_uni_modules,
}
)
console.log('uts插件[' + pkg.id + ']缓存检查耗时:', Date.now() - start)
if (!res.expired) {
if (utsPlatform === 'app-android') {
const cacheFile = resolveDexCacheFile(pluginRelativeDir, outputDir)
if (cacheFile) {
copySync(
cacheFile,
join(outputDir, pluginRelativeDir, basename(cacheFile))
)
}
}
if (res.tips) {
console.warn(res.tips)
}
return {
code,
// 所有文件加入依赖
deps: res.files.map((name) => join(pluginDir, name)),
}
}
}
const filename = const filename =
resolvePlatformIndex(process.env.UNI_UTS_PLATFORM, module, pkg) || resolvePlatformIndex(utsPlatform, pluginDir, pkg) ||
resolveRootIndex(module, pkg) resolveRootIndex(pluginDir, pkg)
const compilerType = const compilerType = utsPlatform === 'app-android' ? 'kotlin' : 'swift'
process.env.UNI_UTS_PLATFORM === 'app-android' ? 'kotlin' : 'swift'
if (filename) { if (filename) {
deps.push(filename) deps.push(filename)
if (utsPlatform === 'app-android') {
deps.push(...resolveAndroidDepFiles(filename))
} else {
deps.push(...resolveIOSDepFiles(filename))
}
const res = await getCompiler(compilerType).runDev(filename) const res = await getCompiler(compilerType).runDev(filename)
if (res) { if (res) {
if (isArray(res.deps) && res.deps.length) { if (isArray(res.deps) && res.deps.length) {
// 添加其他文件的依赖 // 添加其他文件的依赖
deps.push(...res.deps) deps.push(...res.deps)
} }
let isSuccess = false
if (res.type === 'swift') { if (res.type === 'swift') {
if (res.code) { if (res.code) {
errMsg = compileErrMsg(pkg.id) errMsg = compileErrMsg(pkg.id)
...@@ -110,14 +172,31 @@ export async function compile(module: string) { ...@@ -110,14 +172,31 @@ export async function compile(module: string) {
sourceMapFile: resolveUtsPluginSourceMapFile( sourceMapFile: resolveUtsPluginSourceMapFile(
'swift', 'swift',
filename, filename,
process.env.UNI_INPUT_DIR, inputDir,
process.env.UNI_OUTPUT_DIR outputDir
), ),
sourceRoot: process.env.UNI_INPUT_DIR, sourceRoot: inputDir,
})) }))
) )
} else {
isSuccess = true
} }
} else if (res.type === 'kotlin') {
if (res.changed.length) {
isSuccess = true
}
}
// 生成缓存文件
if (cacheDir && isSuccess) {
genManifestFile(utsPlatform, {
pluginDir,
env,
cacheDir,
pluginRelativeDir,
is_uni_modules: pkg.is_uni_modules,
})
} }
const files: string[] = [] const files: string[] = []
if (process.env.UNI_APP_UTS_CHANGED_FILES) { if (process.env.UNI_APP_UTS_CHANGED_FILES) {
try { try {
...@@ -126,6 +205,17 @@ export async function compile(module: string) { ...@@ -126,6 +205,17 @@ export async function compile(module: string) {
} }
if (res.changed && res.changed.length) { if (res.changed && res.changed.length) {
files.push(...res.changed) files.push(...res.changed)
// 需要缓存 dex 文件
if (cacheDir && res.type === 'kotlin') {
res.changed.forEach((file) => {
if (file.endsWith('classes.dex')) {
copySync(
join(outputDir, file),
resolveDexCacheFilename(pluginRelativeDir, outputDir)
)
}
})
}
} else { } else {
if (res.type === 'kotlin') { if (res.type === 'kotlin') {
errMsg = compileErrMsg(pkg.id) errMsg = compileErrMsg(pkg.id)
...@@ -158,3 +248,12 @@ function getCompiler(type: 'kotlin' | 'swift') { ...@@ -158,3 +248,12 @@ function getCompiler(type: 'kotlin' | 'swift') {
runDev: runKotlinDev, runDev: runKotlinDev,
} }
} }
function resolveDexCacheFilename(pluginRelativeDir: string, outputDir: string) {
return join(outputDir, '../.uts/dex', pluginRelativeDir, 'classes.dex')
}
function resolveDexCacheFile(pluginRelativeDir: string, outputDir: string) {
const file = resolveDexCacheFilename(pluginRelativeDir, outputDir)
return (existsSync(file) && file) || ''
}
...@@ -254,6 +254,13 @@ function resolveAndroidManifestPackage(filename: string) { ...@@ -254,6 +254,13 @@ function resolveAndroidManifestPackage(filename: string) {
} }
} }
const deps = ['AndroidManifest.xml', 'config.json']
export function resolveAndroidDepFiles(filename: string) {
const dir = resolveAndroidDir(filename)
return deps.map((dep) => path.resolve(dir, dep))
}
function resolveConfigJsonFile(filename: string) { function resolveConfigJsonFile(filename: string) {
const configJsonFile = path.resolve( const configJsonFile = path.resolve(
resolveAndroidDir(filename), resolveAndroidDir(filename),
......
import { existsSync } from 'fs'
import { join } from 'path'
import { import {
checkManifest, checkManifest,
hasCustomResources, hasCustomResources,
isCustomResources, isCustomResources,
resolveManifestJson, resolveManifestJson,
resolvePluginAndroidFiles, resolvePluginFiles,
} from './manifest' } from './manifest'
import { import {
APP_PLATFORM, APP_PLATFORM,
...@@ -15,9 +13,10 @@ import { ...@@ -15,9 +13,10 @@ import {
customResourceTips, customResourceTips,
} from './utils' } from './utils'
export { genManifestFile } from './manifest'
interface PlatformOptions { interface PlatformOptions {
customRes: string[] customRes: string[]
cacheFile: false | string
} }
const ANDROID_CUSTOM_RES = [ const ANDROID_CUSTOM_RES = [
...@@ -49,25 +48,22 @@ export function checkSwiftCompile( ...@@ -49,25 +48,22 @@ export function checkSwiftCompile(
return checkCompile('app-ios', playground, options) return checkCompile('app-ios', playground, options)
} }
function checkCompile( export function checkCompile(
platform: APP_PLATFORM, platform: APP_PLATFORM,
playground: typeof process.env.HX_USE_BASE_TYPE, playground: typeof process.env.HX_USE_BASE_TYPE,
options: CheckOptions options: CheckOptions
) { ) {
const platformOptions: PlatformOptions = { const platformOptions: PlatformOptions = {
customRes: platform === 'app-android' ? ANDROID_CUSTOM_RES : IOS_CUSTOM_RES, customRes: platform === 'app-android' ? ANDROID_CUSTOM_RES : IOS_CUSTOM_RES,
cacheFile:
platform === 'app-android'
? resolveDexCacheFile(options.pluginRelativeDir, options.outputDir)
: false,
} }
if (playground === 'standard') { if (playground === 'standard') {
return checkWithPlayground('standard', options, platformOptions) return checkWithPlayground(platform, 'standard', options, platformOptions)
} }
return checkWithPlayground('custom', options, platformOptions) return checkWithPlayground(platform, 'custom', options, platformOptions)
} }
async function checkWithPlayground( async function checkWithPlayground(
platform: APP_PLATFORM,
type: typeof process.env.HX_USE_BASE_TYPE, type: typeof process.env.HX_USE_BASE_TYPE,
{ {
id, id,
...@@ -77,27 +73,19 @@ async function checkWithPlayground( ...@@ -77,27 +73,19 @@ async function checkWithPlayground(
pluginRelativeDir, pluginRelativeDir,
is_uni_modules, is_uni_modules,
}: CheckOptions, }: CheckOptions,
{ customRes, cacheFile }: PlatformOptions { customRes }: PlatformOptions
): Promise<CheckResult> { ): Promise<CheckResult> {
// 第一步:获取所有文件列表 // 第一步:获取所有文件列表
const files = await resolvePluginAndroidFiles(pluginDir, is_uni_modules) const files = await resolvePluginFiles(platform, pluginDir, is_uni_modules)
let tips = '' let tips = ''
// 标准基座检查是否包含原生资源/配置 // 标准基座检查是否包含原生资源/配置
if (type === 'standard' && hasCustomResources(files, customRes)) { if (type === 'standard' && hasCustomResources(files, customRes)) {
tips = customResourceTips(id) tips = customResourceTips(id)
} }
// 第二步:检查 dex 文件是否存在 // 第二步:获取当前插件缓存文件信息
if (cacheFile !== false && !cacheFile) { const manifest = resolveManifestJson(platform, pluginRelativeDir, cacheDir)
return { expired: true, tips, cacheFile: '' }
}
// 第三步:获取当前插件缓存文件信息
const manifest = resolveManifestJson(
'app-android',
pluginRelativeDir,
cacheDir
)
if (!manifest) { if (!manifest) {
return { expired: true, tips, cacheFile: '' } return { expired: true, tips, files }
} }
// 第四步:检查文件变更 // 第四步:检查文件变更
const res = await checkManifest(manifest, { env, files, pluginDir }) const res = await checkManifest(manifest, { env, files, pluginDir })
...@@ -110,11 +98,12 @@ async function checkWithPlayground( ...@@ -110,11 +98,12 @@ async function checkWithPlayground(
return { return {
expired: res !== true, expired: res !== true,
tips, tips,
cacheFile, files,
} }
} }
function resolveDexCacheFile(pluginRelativeDir: string, outputDir: string) { export function initCheckOptionsEnv(): CheckOptions['env'] {
const file = join(outputDir, '../.uts/dex', pluginRelativeDir, 'classes.dex') return {
return (existsSync(file) && file) || '' compilerVersion: require('../../package.json').version,
}
} }
...@@ -50,6 +50,15 @@ interface GenManifestJsonOptions { ...@@ -50,6 +50,15 @@ interface GenManifestJsonOptions {
is_uni_modules: boolean is_uni_modules: boolean
} }
export interface GenManifestFileOptions {
cacheDir: string
pluginRelativeDir: string
is_uni_modules: boolean
env: Record<string, unknown>
pluginDir: string
files?: string[]
}
export async function genManifestFile( export async function genManifestFile(
platform: APP_PLATFORM, platform: APP_PLATFORM,
{ {
...@@ -59,14 +68,7 @@ export async function genManifestFile( ...@@ -59,14 +68,7 @@ export async function genManifestFile(
cacheDir, cacheDir,
pluginRelativeDir, pluginRelativeDir,
is_uni_modules, is_uni_modules,
}: { }: GenManifestFileOptions
cacheDir: string
pluginRelativeDir: string
is_uni_modules: boolean
env: Record<string, unknown>
pluginDir: string
files?: string[]
}
) { ) {
outputFileSync( outputFileSync(
resolveManifestFilename(platform, pluginRelativeDir, cacheDir), resolveManifestFilename(platform, pluginRelativeDir, cacheDir),
...@@ -89,11 +91,7 @@ export async function genManifestJson( ...@@ -89,11 +91,7 @@ export async function genManifestJson(
{ pluginDir, files, env, is_uni_modules }: GenManifestJsonOptions { pluginDir, files, env, is_uni_modules }: GenManifestJsonOptions
): Promise<Manifest> { ): Promise<Manifest> {
if (!files) { if (!files) {
if (platform === 'app-android') { files = await resolvePluginFiles(platform, pluginDir, is_uni_modules)
files = await resolvePluginAndroidFiles(pluginDir, is_uni_modules)
} else if (platform === 'app-ios') {
files = await resolvePluginIOSFiles(pluginDir, is_uni_modules)
}
} }
if (!files) { if (!files) {
files = [] files = []
...@@ -137,23 +135,14 @@ async function resolvePluginCommonFiles( ...@@ -137,23 +135,14 @@ async function resolvePluginCommonFiles(
}) })
} }
export async function resolvePluginAndroidFiles( export async function resolvePluginFiles(
pluginDir: string, platform: APP_PLATFORM,
is_uni_modules: boolean
) {
return Promise.all([
resolvePluginCommonFiles(pluginDir, is_uni_modules),
resolvePluginPlatformFiles('app-android', pluginDir, is_uni_modules),
]).then((files) => files.flat())
}
export async function resolvePluginIOSFiles(
pluginDir: string, pluginDir: string,
is_uni_modules: boolean is_uni_modules: boolean
) { ) {
return Promise.all([ return Promise.all([
resolvePluginCommonFiles(pluginDir, is_uni_modules), resolvePluginCommonFiles(pluginDir, is_uni_modules),
resolvePluginPlatformFiles('app-ios', pluginDir, is_uni_modules), resolvePluginPlatformFiles(platform, pluginDir, is_uni_modules),
]).then((files) => files.flat()) ]).then((files) => files.flat())
} }
......
...@@ -13,7 +13,7 @@ export interface CheckOptions { ...@@ -13,7 +13,7 @@ export interface CheckOptions {
export interface CheckResult { export interface CheckResult {
expired: boolean expired: boolean
tips?: string tips?: string
cacheFile: string | false files: string[]
} }
export function customResourceTips(id: string) { export function customResourceTips(id: string) {
......
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
getCompilerServer, getCompilerServer,
getUtsCompiler, getUtsCompiler,
moveRootIndexSourceMap, moveRootIndexSourceMap,
resolveIOSDir,
resolvePackage, resolvePackage,
resolveUTSPlatformFile, resolveUTSPlatformFile,
resolveUTSSourceMapPath, resolveUTSSourceMapPath,
...@@ -162,6 +163,13 @@ export async function compile( ...@@ -162,6 +163,13 @@ export async function compile(
return result return result
} }
const deps = ['Info.plist', 'config.json']
export function resolveIOSDepFiles(filename: string) {
const dir = resolveIOSDir(filename)
return deps.map((dep) => path.resolve(dir, dep))
}
interface SwiftCompilerServer { interface SwiftCompilerServer {
compile(options: { compile(options: {
projectPath: string projectPath: string
......
...@@ -127,6 +127,10 @@ export function resolveAndroidDir(filename: string) { ...@@ -127,6 +127,10 @@ export function resolveAndroidDir(filename: string) {
return resolveUTSPlatformDir(filename, 'app-android') return resolveUTSPlatformDir(filename, 'app-android')
} }
export function resolveIOSDir(filename: string) {
return resolveUTSPlatformDir(filename, 'app-ios')
}
function resolveUTSPlatformDir( function resolveUTSPlatformDir(
filename: string, filename: string,
platform: typeof process.env.UNI_UTS_PLATFORM platform: typeof process.env.UNI_UTS_PLATFORM
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册