Refactoring how feature code is loaded

This commit is contained in:
timongh 2023-01-19 22:19:36 +08:00
parent 1906f687fe
commit c6d7d74a94
6 changed files with 96 additions and 427 deletions

View File

@ -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 () => {

View File

@ -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()

View File

@ -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,
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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')