mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Refactoring how feature code is loaded
This commit is contained in:
parent
1906f687fe
commit
c6d7d74a94
@ -141,19 +141,26 @@ export const loadComponent = async (component: ComponentMetadata) => {
|
||||
/** 加载所有用户组件的定义 (不运行) */
|
||||
export const loadAllUserComponents = async () => {
|
||||
const { settings } = await import('@/core/settings')
|
||||
const { loadFeaturesFromCodes, FeatureKind } = await import(
|
||||
'@/core/external-input/load-features-from-codes'
|
||||
)
|
||||
const { loadFeatureCode } = await import('@/core/external-input/load-feature-code')
|
||||
|
||||
const loadUserComponent = (component: ComponentMetadata) => {
|
||||
components.push(component)
|
||||
componentsMap[component.name] = component
|
||||
}
|
||||
const userComponents = await loadFeaturesFromCodes(
|
||||
FeatureKind.Component,
|
||||
Object.keys(settings.userComponents),
|
||||
Object.values(settings.userComponents).map(it => it.code),
|
||||
)
|
||||
userComponents.forEach(loadUserComponent)
|
||||
|
||||
for (const [name, setting] of Object.entries(settings.userComponents)) {
|
||||
const { code } = setting
|
||||
let metadata: ComponentMetadata
|
||||
try {
|
||||
metadata = loadFeatureCode(code) as ComponentMetadata
|
||||
} catch {
|
||||
console.error(
|
||||
`从代码加载用户组件失败。代码可能有语法错误或代码执行时有抛出值。组件名:'${name}'`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
loadUserComponent(metadata)
|
||||
}
|
||||
}
|
||||
/** 载入所有组件 */
|
||||
export const loadAllComponents = async () => {
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
import { loadFeatureCode, LoadFeatureCodeResult } from '@/core/external-input/load-feature-code'
|
||||
|
||||
type LdRes<X> = LoadFeatureCodeResult<X>
|
||||
type SettledRes<T> = PromiseSettledResult<T>
|
||||
type FilledRes<T> = PromiseFulfilledResult<T>
|
||||
|
||||
const unwrapSettledResult = <T>(r: SettledRes<T>): T => (r as FilledRes<T>).value
|
||||
|
||||
const mapSettledArray = <T>(arr: SettledRes<T>[]): T[] => arr.map(unwrapSettledResult)
|
||||
|
||||
const mapSettleResult = <T>(p: Promise<SettledRes<T>[]>): Promise<T[]> => p.then(mapSettledArray)
|
||||
|
||||
/**
|
||||
* 批量加载组件或插件的代码字符串,获取其导出 feature
|
||||
*
|
||||
* @param codes 代码字符串数组
|
||||
* @returns 不会失败的 `Promise`。其结果为一个数组,其中每个元素都是代表代码执行结果的对象
|
||||
*/
|
||||
export const loadFeatureCodeAllSettled = <X>(
|
||||
codes: string[],
|
||||
): Promise<LoadFeatureCodeResult<X>[]> =>
|
||||
lodash(codes)
|
||||
.map<Promise<LdRes<X>>>(loadFeatureCode)
|
||||
.thru<Promise<SettledRes<LdRes<X>>[]>>(arr => Promise.allSettled(arr))
|
||||
.thru(mapSettleResult)
|
||||
.value()
|
||||
@ -1,156 +0,0 @@
|
||||
import {
|
||||
loadFeatureCode,
|
||||
LoadFeatureCodeResult,
|
||||
LoadFeatureCodeResultError,
|
||||
LoadFeatureCodeResultOk,
|
||||
} from '@/core/external-input/load-feature-code'
|
||||
import { FeatureBase } from '@/components/types'
|
||||
|
||||
interface ResultInstance {
|
||||
readonly isOk: <X extends FeatureBase>(
|
||||
this: LoadFeatureCodeAllResult<X>,
|
||||
) => this is LoadFeatureCodeAllResultOk<X>
|
||||
|
||||
readonly isError: (
|
||||
this: LoadFeatureCodeAllResult<FeatureBase>,
|
||||
) => this is LoadFeatureCodeAllResultError
|
||||
|
||||
readonly isNoExport: (
|
||||
this: LoadFeatureCodeAllResult<FeatureBase>,
|
||||
) => this is LoadFeatureCodeAllResultNoExport
|
||||
|
||||
readonly isCodeThrew: (
|
||||
this: LoadFeatureCodeAllResult<FeatureBase>,
|
||||
) => this is LoadFeatureCodeAllResultCodeThrew
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功从代码中获取 features
|
||||
*
|
||||
* @namespace
|
||||
* @property features 从代码中获取的导出值
|
||||
*/
|
||||
interface LoadFeatureCodeAllResultOk<X extends FeatureBase> extends ResultInstance {
|
||||
readonly tag: 'Ok'
|
||||
readonly features: X[]
|
||||
}
|
||||
|
||||
/** 代码没有导出任何值 */
|
||||
interface LoadFeatureCodeAllResultNoExport extends ResultInstance {
|
||||
readonly tag: 'NoExport'
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行代码过程中产生了抛出值。
|
||||
*
|
||||
* @namespace
|
||||
* @property thrown 抛出的值
|
||||
*/
|
||||
interface LoadFeatureCodeAllResultCodeThrew extends ResultInstance {
|
||||
readonly tag: 'CodeThrew'
|
||||
readonly thrown: unknown
|
||||
}
|
||||
|
||||
type LoadFeatureCodeAllResultError =
|
||||
| LoadFeatureCodeAllResultNoExport
|
||||
| LoadFeatureCodeAllResultCodeThrew
|
||||
type LoadFeatureCodeAllResult<X extends FeatureBase> =
|
||||
| LoadFeatureCodeAllResultOk<X>
|
||||
| LoadFeatureCodeAllResultError
|
||||
|
||||
const resultProto: ResultInstance = {
|
||||
isOk() {
|
||||
return this.tag === 'Ok'
|
||||
},
|
||||
isError() {
|
||||
return this.tag !== 'Ok'
|
||||
},
|
||||
isNoExport() {
|
||||
return this.tag === 'NoExport'
|
||||
},
|
||||
isCodeThrew() {
|
||||
return this.tag === 'CodeThrew'
|
||||
},
|
||||
}
|
||||
|
||||
const okResult = <X extends FeatureBase>(features: X[]): LoadFeatureCodeAllResultOk<X> =>
|
||||
lodash.create(resultProto, {
|
||||
tag: 'Ok' as const,
|
||||
features,
|
||||
})
|
||||
|
||||
const noExportResult = lodash.create(resultProto, {
|
||||
tag: 'NoExport' as const,
|
||||
})
|
||||
|
||||
const codeThrewResult = (thrown: unknown): LoadFeatureCodeAllResultCodeThrew =>
|
||||
lodash.create(resultProto, {
|
||||
tag: 'CodeThrew' as const,
|
||||
thrown,
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type Task<Ok, Err = never> = Promise<Ok>
|
||||
|
||||
type LdRes<X> = LoadFeatureCodeResult<X>
|
||||
type LdOk<X> = LoadFeatureCodeResultOk<X>
|
||||
type LdErr = LoadFeatureCodeResultError
|
||||
|
||||
type LdAllRes<X> = LoadFeatureCodeAllResult<X>
|
||||
type LdAllOk<X> = LoadFeatureCodeAllResultOk<X>
|
||||
type LdAllErr = LoadFeatureCodeAllResultError
|
||||
|
||||
type LoadCodesTask<X> = Task<LdOk<X>[], [number, LdErr]>
|
||||
|
||||
// covert `Task<LdRes<X>>` to `Task<LdOk<X>, LdErr>`
|
||||
const rejectErrorResult = <X>(t: Task<LdRes<X>>): Task<LdOk<X>, LdErr> =>
|
||||
t.then(r => (r.isOk() ? r : Promise.reject(r)))
|
||||
|
||||
// load feature code, and return `Task<LdOk<X>, LdErr>`
|
||||
const loadCode = <X>(code: string): Task<LdOk<X>, LdErr> => rejectErrorResult(loadFeatureCode(code))
|
||||
|
||||
// covert `Task`'s `Err` type from `T` to `[number, T]`
|
||||
const addIndexToRejected = <N extends number, O, E>(
|
||||
t: Task<O, E>,
|
||||
i: N,
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
): Task<O, [N, E]> => t.catch(e => Promise.reject([i, e]))
|
||||
|
||||
// create `LdAllOk` from an array of `LdOk`
|
||||
const createOkResult = <X>(arr: LdOk<X>[]): LdAllOk<X> => okResult(arr.map(r => r.feature))
|
||||
|
||||
// create `LdAllErr` from `[number, LdErr]`
|
||||
const createErrResult = (t: [number, LdErr]): LdAllErr =>
|
||||
t[1].isCodeThrew() ? codeThrewResult(t[1].thrown) : noExportResult
|
||||
|
||||
// load all feature codes, and return `LoadCodesTask`
|
||||
const loadCodes = <X>(codes: string[]): LoadCodesTask<X> =>
|
||||
lodash(codes)
|
||||
.map<Task<LdOk<X>, LdErr>>(loadCode)
|
||||
.map(addIndexToRejected)
|
||||
.thru<LoadCodesTask<X>>(arr => Promise.all(arr))
|
||||
.value()
|
||||
|
||||
// create a `LdAllRes` wrapped by `Task`
|
||||
const createTaskResult = <X>(t: LoadCodesTask<X>): Task<LdAllRes<X>> =>
|
||||
t.then(createOkResult).catch(createErrResult)
|
||||
|
||||
/**
|
||||
* 批量加载组件或插件的代码字符串,获取其导出 feature
|
||||
*
|
||||
* 只要有一个代码出现了错误,则返回错误。
|
||||
*
|
||||
* @param codes 代码字符串数组
|
||||
* @returns 一个不会失败的 `Promise`,其结果值为 `LoadFeatureCodeAllResult`
|
||||
*/
|
||||
const loadFeatureCodeAll = <X>(codes: string[]): Promise<LoadFeatureCodeAllResult<X>> =>
|
||||
lodash(codes).thru<LoadCodesTask<X>>(loadCodes).thru(createTaskResult).value()
|
||||
|
||||
export {
|
||||
loadFeatureCodeAll,
|
||||
LoadFeatureCodeAllResult,
|
||||
LoadFeatureCodeAllResultOk,
|
||||
LoadFeatureCodeAllResultError,
|
||||
LoadFeatureCodeAllResultNoExport,
|
||||
LoadFeatureCodeAllResultCodeThrew,
|
||||
}
|
||||
@ -1,116 +1,70 @@
|
||||
import { FeatureBase } from '@/components/types'
|
||||
import { ComponentMetadata } from '@/components/types'
|
||||
import { PluginMetadata } from '@/plugins/plugin'
|
||||
import { UserStyle } from '@/plugins/style'
|
||||
|
||||
interface ResultInstance {
|
||||
readonly isOk: <X extends FeatureBase>(
|
||||
this: LoadFeatureCodeResult<X>,
|
||||
) => this is LoadFeatureCodeResultOk<X>
|
||||
|
||||
readonly isError: (this: LoadFeatureCodeResult<FeatureBase>) => this is LoadFeatureCodeResultError
|
||||
|
||||
readonly isNoExport: (
|
||||
this: LoadFeatureCodeResult<FeatureBase>,
|
||||
) => this is LoadFeatureCodeResultNoExport
|
||||
|
||||
readonly isCodeThrew: (
|
||||
this: LoadFeatureCodeResult<FeatureBase>,
|
||||
) => this is LoadFeatureCodeResultCodeThrew
|
||||
}
|
||||
export class LoadFeatureError extends Error {}
|
||||
|
||||
/**
|
||||
* 成功从代码中获取 feature
|
||||
* 执行 feature (component, plugin, style) 的代码,并尝试获取其导出元数据
|
||||
*
|
||||
* @namespace
|
||||
* @property feature 从代码中获取的导出值
|
||||
*/
|
||||
interface LoadFeatureCodeResultOk<X extends FeatureBase> extends ResultInstance {
|
||||
readonly tag: 'Ok'
|
||||
readonly feature: X
|
||||
}
|
||||
|
||||
/** 代码没有导出任何值 */
|
||||
interface LoadFeatureCodeResultNoExport extends ResultInstance {
|
||||
readonly tag: 'NoExport'
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行代码过程中产生了抛出值。
|
||||
* @remarks
|
||||
* feature 代码支持两种导出格式:
|
||||
* 1. 在本项目中打包 feature 所使用的导出格式
|
||||
* 2. 若代码整体为一个表达式,则导出表达式的返回值
|
||||
*
|
||||
* @namespace
|
||||
* @property thrown 抛出的值
|
||||
* @param code - 被执行的代码
|
||||
* @returns 导出的元数据
|
||||
* @throws {@link LoadFeatureError} 代码抛出了一个值或代码存在语法错误
|
||||
*/
|
||||
interface LoadFeatureCodeResultCodeThrew extends ResultInstance {
|
||||
readonly tag: 'CodeThrew'
|
||||
readonly thrown: unknown
|
||||
}
|
||||
|
||||
type LoadFeatureCodeResultError = LoadFeatureCodeResultNoExport | LoadFeatureCodeResultCodeThrew
|
||||
type LoadFeatureCodeResult<X extends FeatureBase> =
|
||||
| LoadFeatureCodeResultOk<X>
|
||||
| LoadFeatureCodeResultError
|
||||
|
||||
const resultProto: ResultInstance = {
|
||||
isOk() {
|
||||
return this.tag === 'Ok'
|
||||
},
|
||||
isError() {
|
||||
return this.tag !== 'Ok'
|
||||
},
|
||||
isNoExport() {
|
||||
return this.tag === 'NoExport'
|
||||
},
|
||||
isCodeThrew() {
|
||||
return this.tag === 'CodeThrew'
|
||||
},
|
||||
}
|
||||
|
||||
const okResult = <X extends FeatureBase>(feature: X): LoadFeatureCodeResultOk<X> =>
|
||||
lodash.create(resultProto, {
|
||||
tag: 'Ok' as const,
|
||||
feature,
|
||||
})
|
||||
|
||||
const noExportResult = lodash.create(resultProto, {
|
||||
tag: 'NoExport' as const,
|
||||
})
|
||||
|
||||
const codeThrewResult = (thrown: unknown): LoadFeatureCodeResultCodeThrew =>
|
||||
lodash.create(resultProto, {
|
||||
tag: 'CodeThrew' as const,
|
||||
thrown,
|
||||
})
|
||||
|
||||
/**
|
||||
* 加载组件或插件的代码字符串,获取其导出 feature
|
||||
*
|
||||
* @param code 代码字符串
|
||||
* @returns 一个不会失败的 `Promise`,其结果值为 {@link LoadFeatureCodeResult}
|
||||
*/
|
||||
const loadFeatureCode = async <X extends FeatureBase>(
|
||||
code: string,
|
||||
): Promise<LoadFeatureCodeResult<X>> => {
|
||||
// 收集代码导出值
|
||||
const exports = {}
|
||||
let result: X
|
||||
try {
|
||||
result = eval(code)
|
||||
} catch (thrown) {
|
||||
return codeThrewResult(thrown)
|
||||
}
|
||||
const values = Object.values(exports)
|
||||
if (values.length === 0) {
|
||||
if (typeof result === 'object') {
|
||||
return okResult(result)
|
||||
export const loadFeatureCode = (code: string): ComponentMetadata | PluginMetadata | UserStyle => {
|
||||
// 将 `key` 和 `val` 临时赋值到 `target` 上并返回
|
||||
// 调用返回值中的 restore 函数,可以恢复 `target` 中该属性的原始情况(包括属性不存在的情况)
|
||||
const temporarilySet = <O extends object, K extends keyof any, V>(
|
||||
target: O,
|
||||
key: K,
|
||||
val: V,
|
||||
): { target: O & Record<K, V>; restore(): void } => {
|
||||
const target0 = target as { [K0 in K]?: V }
|
||||
let restore
|
||||
if (key in target0) {
|
||||
const org = target0[key]
|
||||
target0[key] = val
|
||||
restore = () => {
|
||||
target0[key] = org
|
||||
}
|
||||
} else {
|
||||
target0[key] = val
|
||||
restore = () => {
|
||||
delete target0[key]
|
||||
}
|
||||
}
|
||||
return {
|
||||
target: target0 as O & Record<K, V>,
|
||||
restore,
|
||||
}
|
||||
return noExportResult
|
||||
}
|
||||
return okResult(values[0] as X)
|
||||
}
|
||||
|
||||
export {
|
||||
loadFeatureCode,
|
||||
LoadFeatureCodeResult,
|
||||
LoadFeatureCodeResultOk,
|
||||
LoadFeatureCodeResultError,
|
||||
LoadFeatureCodeResultNoExport,
|
||||
LoadFeatureCodeResultCodeThrew,
|
||||
// to save what the code exports
|
||||
const exports = {}
|
||||
// value to return.
|
||||
let result: unknown
|
||||
const { restore } = temporarilySet(window, 'exports', exports)
|
||||
const gEval = eval
|
||||
// eval code
|
||||
try {
|
||||
result = gEval(code)
|
||||
} catch {
|
||||
throw new LoadFeatureError()
|
||||
} finally {
|
||||
// restore window.exports
|
||||
restore()
|
||||
}
|
||||
|
||||
// set the value code exported to variable `result` if it exists
|
||||
const values = Object.values(exports)
|
||||
if (values.length !== 0) {
|
||||
result = values[0]
|
||||
}
|
||||
|
||||
return result as ComponentMetadata | PluginMetadata | UserStyle
|
||||
}
|
||||
|
||||
@ -1,114 +0,0 @@
|
||||
import {
|
||||
LoadFeatureCodeResultError,
|
||||
LoadFeatureCodeResultOk,
|
||||
} from '@/core/external-input/load-feature-code'
|
||||
import { useScopedConsole } from '@/core/utils/log'
|
||||
import { ComponentMetadata } from '@/components/types'
|
||||
import { PluginMetadata } from '@/plugins/plugin'
|
||||
import { loadFeatureCodeAllSettled } from '@/core/external-input/load-feature-code-all-settled'
|
||||
|
||||
const curConsole = useScopedConsole('@/core/external-input/load-features-from-codes.ts')
|
||||
|
||||
export enum FeatureKind {
|
||||
Component = 'Component',
|
||||
Plugin = 'Plugin',
|
||||
}
|
||||
|
||||
const logError = (
|
||||
kind: FeatureKind,
|
||||
): ((featureName: string, err: LoadFeatureCodeResultError) => void) => {
|
||||
const prefix = kind === FeatureKind.Component ? 'component' : 'plugin'
|
||||
return (featureName, err) => {
|
||||
if (err.isNoExport()) {
|
||||
curConsole.error(`${prefix} '${featureName}' exports no value, failed to load`)
|
||||
} else {
|
||||
curConsole.error(
|
||||
`${prefix} '${featureName}' throws something when importing, failed to load`,
|
||||
{ thrown: err.thrown },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reportErrToUser = (featureKind: FeatureKind, errNames: string[]): void => {
|
||||
type ErrInfo = number | string[]
|
||||
|
||||
const emptyErrInfo: () => string[] = () => []
|
||||
|
||||
const accErrInfo = (acc: ErrInfo, featureName: string): ErrInfo => {
|
||||
if (Array.isArray(acc)) {
|
||||
if (acc.length < 3) {
|
||||
acc.push(featureName)
|
||||
return acc
|
||||
}
|
||||
return 4
|
||||
}
|
||||
return acc + 1
|
||||
}
|
||||
|
||||
const reportErrInfo = async (kind: FeatureKind, info: ErrInfo) => {
|
||||
const { Toast } = await import('../toast')
|
||||
const kindName = kind === FeatureKind.Component ? '组件' : '插件'
|
||||
if (Array.isArray(info)) {
|
||||
Toast.error(
|
||||
`${kindName} "${info.join('", "')}" 加载失败。请向我们反馈,以解决此问题。`,
|
||||
`${kindName}加载失败`,
|
||||
)
|
||||
} else {
|
||||
Toast.error(
|
||||
`有 ${info} 个${kindName}加载失败,请向我们反馈,以解决此问题。`,
|
||||
`${kindName}加载失败`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const errInfo = errNames.reduce(accErrInfo, emptyErrInfo())
|
||||
reportErrInfo(featureKind, errInfo)
|
||||
}
|
||||
|
||||
export type FeatureMetadata = ComponentMetadata | PluginMetadata
|
||||
|
||||
/**
|
||||
* 批量加载组件或插件代码
|
||||
*
|
||||
* 如果遇到错误会向 console 和用户输出错误信息
|
||||
*
|
||||
* `names` 和 `codes` 应该是一一对应的
|
||||
*
|
||||
* @param kind 组件或插件类型
|
||||
* @param names 组件或插件名称
|
||||
* @param codes 组件或插件代码
|
||||
* @return 返回加载成功的组件或插件
|
||||
*/
|
||||
export async function loadFeaturesFromCodes(
|
||||
kind: FeatureKind.Component,
|
||||
names: string[],
|
||||
codes: string[],
|
||||
): Promise<ComponentMetadata[]>
|
||||
export async function loadFeaturesFromCodes(
|
||||
kind: FeatureKind.Plugin,
|
||||
names: string[],
|
||||
codes: string[],
|
||||
): Promise<PluginMetadata[]>
|
||||
export async function loadFeaturesFromCodes(
|
||||
kind: FeatureKind,
|
||||
names: string[],
|
||||
codes: string[],
|
||||
): Promise<FeatureMetadata[]> {
|
||||
const results = await loadFeatureCodeAllSettled<FeatureMetadata>(codes)
|
||||
const [namedOk, namedErr] = lodash(results)
|
||||
.map((r, i) => [names[i], r] as const)
|
||||
.partition(([, r]) => r.isOk())
|
||||
.value()
|
||||
|
||||
// 输出日志
|
||||
lodash.forEach(namedErr, lodash.spread(logError(kind)))
|
||||
|
||||
// 向用户输出错误报告
|
||||
if (namedErr.length > 0) {
|
||||
const errNames = namedErr.map(([name]) => name)
|
||||
reportErrToUser(kind, errNames)
|
||||
}
|
||||
|
||||
return lodash.map(namedOk, ([, r]) => (r as LoadFeatureCodeResultOk<FeatureMetadata>).feature)
|
||||
}
|
||||
@ -155,22 +155,26 @@ export const loadPlugin = async (plugin: PluginMetadata) => {
|
||||
*/
|
||||
export const loadAllPlugins = async (components: ComponentMetadata[]) => {
|
||||
const { settings, getGeneralSettings } = await import('@/core/settings')
|
||||
const { loadFeaturesFromCodes, FeatureKind } = await import(
|
||||
'@/core/external-input/load-features-from-codes'
|
||||
)
|
||||
const otherPlugins = lodash(components)
|
||||
.map(extractPluginFromComponent)
|
||||
.filter(p => p !== null)
|
||||
.map(p => p as PluginMetadata)
|
||||
.concat(
|
||||
await loadFeaturesFromCodes(
|
||||
FeatureKind.Plugin,
|
||||
Object.keys(settings.userPlugins),
|
||||
Object.values(settings.userPlugins).map(p => p.code),
|
||||
),
|
||||
)
|
||||
.value()
|
||||
plugins.push(...otherPlugins)
|
||||
const { loadFeatureCode } = await import('@/core/external-input/load-feature-code')
|
||||
for (const component of components) {
|
||||
const plugin = extractPluginFromComponent(component)
|
||||
if (plugin) {
|
||||
plugins.push(plugin)
|
||||
}
|
||||
}
|
||||
for (const [name, setting] of Object.entries(settings.userPlugins)) {
|
||||
const { code } = setting
|
||||
let metadata: PluginMetadata
|
||||
try {
|
||||
metadata = loadFeatureCode(code) as PluginMetadata
|
||||
} catch {
|
||||
console.error(
|
||||
`从代码加载用户插件失败。代码可能有语法错误或代码执行时有抛出值。插件名:'${name}'`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
plugins.push(metadata)
|
||||
}
|
||||
return Promise.allSettled(plugins.map(loadPlugin)).then(async () => {
|
||||
if (getGeneralSettings().devMode) {
|
||||
const { pluginLoadTime, pluginResolveTime } = await import('@/core/performance/plugin-trace')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user