From 7b4201475aa1f4a826849a2355715d544422e686 Mon Sep 17 00:00:00 2001 From: timongh <46739861+timongh@users.noreply.github.com> Date: Tue, 29 Mar 2022 15:12:02 +0800 Subject: [PATCH 1/5] Change the way of feature code loading --- src/components/component.ts | 40 +++-- .../index.ts} | 21 +-- .../load-feature-code-all-settled.ts | 25 +++ .../external-input/load-feature-code-all.ts | 148 ++++++++++++++++++ src/core/external-input/load-feature-code.ts | 78 +++++++++ .../load-features-from-codes.ts | 116 ++++++++++++++ src/plugins/plugin.ts | 41 ++--- 7 files changed, 418 insertions(+), 51 deletions(-) rename src/core/{external-input.ts => external-input/index.ts} (73%) create mode 100644 src/core/external-input/load-feature-code-all-settled.ts create mode 100644 src/core/external-input/load-feature-code-all.ts create mode 100644 src/core/external-input/load-feature-code.ts create mode 100644 src/core/external-input/load-features-from-codes.ts diff --git a/src/components/component.ts b/src/components/component.ts index 0f2f66c75..f9dad14d1 100644 --- a/src/components/component.ts +++ b/src/components/component.ts @@ -1,10 +1,10 @@ import { addComponentListener, + getComponentSettings, getGeneralSettings, isComponentEnabled, - getComponentSettings, } from '@/core/settings' -import { fullyLoaded, contentLoaded } from '@/core/life-cycle' +import { contentLoaded, fullyLoaded } from '@/core/life-cycle' import { LoadingMode } from '@/core/loading-mode' import { Widget } from '@/components/widget' import { ComponentMetadata } from './types' @@ -26,7 +26,10 @@ const loadI18n = async (component: ComponentMetadata) => { } const { addI18nData } = await import('@/components/i18n/helpers') Object.entries(component.i18n).forEach(([language, data]) => { - const { map = [], regex = [] } = data + const { + map = [], + regex = [], + } = data addI18nData(language, map, regex) }) } @@ -35,14 +38,20 @@ const getComponentWidgetName = (component: ComponentMetadata) => `${component.na /** 提取组件中的Widget */ const loadWidget = async (component: ComponentMetadata) => { if (component.widget) { - const widget: Widget = { ...component.widget, name: getComponentWidgetName(component) } + const widget: Widget = { + ...component.widget, + name: getComponentWidgetName(component), + } const { addData } = await import('@/plugins/data') const { WidgetsPlugin } = await import('./settings-panel') addData(WidgetsPlugin, (widgets: Widget[]) => { if (widgets.find(w => w.name === widget.name)) { return } - const { urlInclude, urlExclude } = widget + const { + urlInclude, + urlExclude, + } = widget if (component.urlInclude) { if (urlInclude) { urlInclude.push(...component.urlInclude) @@ -66,7 +75,7 @@ const loadedComponents: Record> = {} /** 获取已加载组件的导出内容 * @param name 组件名称 */ -export const importComponent = ( +export const importComponent = ( name: string, ): Record => { if (!(name in loadedComponents)) { @@ -137,12 +146,17 @@ export const loadComponent = async (component: ComponentMetadata) => { /** 加载所有用户组件的定义 (不运行) */ export const loadAllUserComponents = async () => { const { settings } = await import('@/core/settings') - const { batchParseCode } = await import('@/core/external-input') + const { + loadFeaturesFromCodes, + FeatureKind, + } = await import('@/core/external-input/load-features-from-codes') const loadUserComponent = (component: ComponentMetadata) => { components.push(component) componentsMap[component.name] = component } - const userComponents = await batchParseCode( + const userComponents = await loadFeaturesFromCodes( + FeatureKind.Component, + Object.keys(settings.userComponents), Object.values(settings.userComponents).map(it => it.code), ) userComponents.forEach(loadUserComponent) @@ -153,12 +167,12 @@ export const loadAllComponents = async () => { const { loadAllPlugins } = await import('@/plugins/plugin') const loadComponents = () => loadAllPlugins(components) .then(() => Promise.allSettled(components.map(loadI18n))) - .then(() => Promise.allSettled(components.map(loadComponent))) - .then(async () => { + .then(() => Promise.allSettled(components.map(loadComponent))).then(async () => { if (generalSettings.devMode) { - const { componentLoadTime, componentResolveTime } = await import( - '@/core/performance/component-trace' - ) + const { + componentLoadTime, + componentResolveTime, + } = await import('@/core/performance/component-trace') const { logStats } = await import('@/core/performance/stats') logStats('components block', componentLoadTime) logStats('components resolve', componentResolveTime) diff --git a/src/core/external-input.ts b/src/core/external-input/index.ts similarity index 73% rename from src/core/external-input.ts rename to src/core/external-input/index.ts index 8eb58db07..1a56cd3c1 100644 --- a/src/core/external-input.ts +++ b/src/core/external-input/index.ts @@ -1,4 +1,4 @@ -import { pickFile } from './file-picker' +import { pickFile } from '@/core/file-picker' /** 外部输入包装类型, 详见`parseExternalInput`的文档 */ export type ExternalInput = undefined | string | T @@ -54,22 +54,3 @@ export const parseExternalInput = async (input: ExternalInput): Promise return input } } -export const batchParseCode = async(inputs: string[]): Promise => { - try { - const exports = {} - const result = inputs.map(input => eval(input) as T) - if (Object.values(exports).length > 0) { - const { coreApis } = await import('@/core/core-apis') - return Object.values(exports).map(value => { - if (typeof value === 'function') { - return value(coreApis) as T - } - return value as T - }) - } - return result - } catch (error) { - console.error(error) - return null - } -} diff --git a/src/core/external-input/load-feature-code-all-settled.ts b/src/core/external-input/load-feature-code-all-settled.ts new file mode 100644 index 000000000..71d70b7bd --- /dev/null +++ b/src/core/external-input/load-feature-code-all-settled.ts @@ -0,0 +1,25 @@ +import { loadFeatureCode, LoadFeatureCodeResult } from '@/core/external-input/load-feature-code' + +type LdRes = LoadFeatureCodeResult +type SettledRes = PromiseSettledResult +type FilledRes = PromiseFulfilledResult + +const unwrapSettledResult = (r: SettledRes): T => (r as FilledRes).value + +const mapSettledArray = (arr: SettledRes[]): T[] => + arr.map(unwrapSettledResult) + +const mapSettleResult = (p: Promise[]>): Promise => + p.then(mapSettledArray) + +/** + * 批量加载组件或插件的代码字符串,获取其导出 feature + * + * @param codes 代码字符串数组 + * @returns 不会失败的 `Promise`。其结果为一个数组,其中每个元素都是代表代码执行结果的对象 + */ +export const loadFeatureCodeAllSettled = (codes: string[]): Promise[]> => lodash(codes) + .map>>(loadFeatureCode) + .thru>[]>>(arr => Promise.allSettled(arr)) + .thru(mapSettleResult) + .value() diff --git a/src/core/external-input/load-feature-code-all.ts b/src/core/external-input/load-feature-code-all.ts new file mode 100644 index 000000000..35031b707 --- /dev/null +++ b/src/core/external-input/load-feature-code-all.ts @@ -0,0 +1,148 @@ +import { + loadFeatureCode, + LoadFeatureCodeError, + LoadFeatureCodeResult, + LoadFeatureCodeResultOk, +} from '@/core/external-input/load-feature-code' +import { FeatureBase } from '@/components/types' + +// ========== result type definitions ========== + +class LoadFeatureCodeAllResultConstructor { + isOk(): this is LoadFeatureCodeAllResultOk { + return this instanceof LoadFeatureCodeAllResultOk + } + + isError(): this is LoadFeatureCodeAllError { + return !this.isOk() + } + + isNoExport(): this is LoadFeatureCodeAllResultNoExport { + return this instanceof LoadFeatureCodeAllResultNoExport + } + + isCodeThrew(): this is LoadFeatureCodeAllResultCodeThrew { + return this instanceof LoadFeatureCodeAllResultCodeThrew + } + + tryGetFeatures(): X[] | undefined { + if (this.isOk()) { + return this.features + } + return undefined + } +} + +/** + * 成活从代码中获取 features + * + * @member features 从代码中获取的导出值 + */ +export class LoadFeatureCodeAllResultOk extends LoadFeatureCodeAllResultConstructor { + constructor(public readonly features: X[]) { + super() + } +} + +/** + * 代码没有导出任何值 + * + * @member index 出错代码对应的代码数组下标 + */ +export class LoadFeatureCodeAllResultNoExport extends LoadFeatureCodeAllResultConstructor { + constructor(public readonly index: number) { + super() + } +} + +/** + * 执行代码过程中产生了抛出值。 + * + * @member index 出错代码对应的代码数组索引 + * @member thrown 抛出的值 + */ +export class LoadFeatureCodeAllResultCodeThrew extends LoadFeatureCodeAllResultConstructor { + constructor(public readonly index: number, public readonly thrown: unknown) { + super() + } +} + +export type LoadFeatureCodeAllError = + LoadFeatureCodeAllResultNoExport + | LoadFeatureCodeAllResultCodeThrew +export type LoadFeatureCodeAllResult = + LoadFeatureCodeAllResultOk + | LoadFeatureCodeAllError + +// ========== internal definitions ========== + +type Task = Promise + +type LdRes = LoadFeatureCodeResult +type LdOk = LoadFeatureCodeResultOk +type LdErr = LoadFeatureCodeError + +type LdAllRes = LoadFeatureCodeAllResult +type LdAllOk = LoadFeatureCodeAllResultOk +type LdAllErr = LoadFeatureCodeAllError + +type LoadCodesTask = Task[], [number, LdErr]> + +const LdCodeThrew = LoadFeatureCodeAllResultCodeThrew +const LdAllOk = LoadFeatureCodeAllResultOk +const LdAllNoExport = LoadFeatureCodeAllResultNoExport +const LdAllCodeThrew = LoadFeatureCodeAllResultCodeThrew + +// covert `Task>` to `Task, LdErr>` +const rejectErrorResult = (t: Task>): Task, LdErr> => ( + t.then(r => r.isOk() ? r : Promise.reject(r)) +) + +// load feature code, and return `Task, LdErr>` +const loadCode = (code: string): Task, LdErr> => ( + rejectErrorResult(loadFeatureCode(code)) +) + +// covert `Task`'s `Err` type from `T` to `[number, T]` +const addIndexToRejected = (t: Task, i: N): Task => ( + t.catch(e => Promise.reject([i, e])) +) + +// create `LdAllOk` from an array of `LdOk` +const createOkResult = (arr: LdOk[]): LdAllOk => ( + new LdAllOk(arr.map(r => r.feature)) +) + +// create `LdAllErr` from `[number, LdErr]` +const createErrResult = (t: [number, LdErr]): LdAllErr => ( + t[1] instanceof LdCodeThrew + ? new LdAllCodeThrew(t[0], t[1].thrown) + : new LdAllNoExport(t[0]) +) + +// load all feature codes, and return `LoadCodesTask` +const loadCodes = (codes: string[]): LoadCodesTask => lodash(codes) + .map, LdErr>>(loadCode) + .map(addIndexToRejected) + .thru>(arr => Promise.all(arr)) + .value() + +// create a `LdAllRes` wrapped by `Task` +const createTaskResult = (t: LoadCodesTask): Task> => t + .then(createOkResult) + .catch(createErrResult) + +// ========== paramount function to export ========== + +/** + * 批量加载组件或插件的代码字符串,获取其导出 feature + * + * 只要有一个代码出现了错误,则返回错误。 + * + * @param codes 代码字符串数组 + * @returns 一个不会失败的 `Promise`,其结果值为 `LoadFeatureCodeAllResult` + */ +export const loadFeatureCodeAll = (codes: string[]): Promise> => lodash(codes) + .thru>(loadCodes) + .thru(createTaskResult) + .value() diff --git a/src/core/external-input/load-feature-code.ts b/src/core/external-input/load-feature-code.ts new file mode 100644 index 000000000..fc8b4778d --- /dev/null +++ b/src/core/external-input/load-feature-code.ts @@ -0,0 +1,78 @@ +import { FeatureBase } from '@/components/types' + +class LoadFeatureCodeResultConstructor { + isOk(): this is LoadFeatureCodeResultOk { + return this instanceof LoadFeatureCodeResultOk + } + + isError(): this is LoadFeatureCodeError { + return !this.isOk() + } + + isNoExport(): this is LoadFeatureCodeResultNoExport { + return this instanceof LoadFeatureCodeResultNoExport + } + + isCodeThrew(): this is LoadFeatureCodeResultCodeThrew { + return this instanceof LoadFeatureCodeResultCodeThrew + } + + tryGetFeature(): X | undefined { + if (this.isOk()) { + return this.feature + } + return undefined + } +} + +/** + * 成功从代码中获取 feature + * + * @member feature 从代码中获取的导出值 + */ +export class LoadFeatureCodeResultOk extends LoadFeatureCodeResultConstructor { + constructor(public readonly feature: X) { + super() + } +} + +/** 代码没有导出任何值 */ +export class LoadFeatureCodeResultNoExport extends LoadFeatureCodeResultConstructor { +} + +/** + * 执行代码过程中产生了抛出值。 + * + * @member {unknown} thrown 抛出的值 + */ +export class LoadFeatureCodeResultCodeThrew extends LoadFeatureCodeResultConstructor { + constructor(public readonly thrown: unknown) { + super() + } +} + +export type LoadFeatureCodeError = LoadFeatureCodeResultNoExport | LoadFeatureCodeResultCodeThrew +export type LoadFeatureCodeResult = + LoadFeatureCodeResultOk + | LoadFeatureCodeError + +/** + * 加载组件或插件的代码字符串,获取其导出 feature + * + * @param code 代码字符串 + * @returns 一个不会失败的 `Promise`,其结果值为 {@link LoadFeatureCodeResult} + */ +export const loadFeatureCode = async (code: string): Promise> => { + // 收集代码导出值 + const exports = {} + try { + eval(code) + } catch (thrown) { + return new LoadFeatureCodeResultCodeThrew(thrown) + } + const values = Object.values(exports) + if (values.length === 0) { + return new LoadFeatureCodeResultNoExport() + } + return new LoadFeatureCodeResultOk(values[0] as X) +} diff --git a/src/core/external-input/load-features-from-codes.ts b/src/core/external-input/load-features-from-codes.ts new file mode 100644 index 000000000..df1929204 --- /dev/null +++ b/src/core/external-input/load-features-from-codes.ts @@ -0,0 +1,116 @@ +import { + LoadFeatureCodeError, + LoadFeatureCodeResultNoExport, + LoadFeatureCodeResultOk, +} from '@/core/external-input/load-feature-code' +import { Toast } from '@/core/toast' +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: LoadFeatureCodeError) => void => { + const prefix = kind === FeatureKind.Component ? 'component' : 'plugin' + return (featureName, err) => { + if (err instanceof LoadFeatureCodeResultNoExport) { + 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 + } else { + return 4 + } + } else { + return acc + 1 + } + } + + const reportErrInfo = (featureKind: FeatureKind, info: ErrInfo): void => { + const kindName = featureKind === 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) +} + +type KindToType = + K extends FeatureKind.Component ? ComponentMetadata : PluginMetadata + +/** + * 批量加载组件或插件代码 + * + * 如果遇到错误会向 console 和用户输出错误信息 + * + * `names` 和 `codes` 应该是一一对应的 + * + * @param kind 组件或插件类型 + * @param names 组件或插件名称 + * @param codes 组件或插件代码 + * @return 返回加载成功的组件或插件 + */ +export async function loadFeaturesFromCodes( + kind: FeatureKind.Component, + names: string[], + codes: string[], +): Promise +export async function loadFeaturesFromCodes( + kind: FeatureKind.Plugin, + names: string[], + codes: string[], +): Promise +export async function loadFeaturesFromCodes( + kind: FeatureKind, + names: string[], + codes: string[], +): Promise<(ComponentMetadata | PluginMetadata)[]> { + const results = await loadFeatureCodeAllSettled>(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>).feature, + ) +} diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index 528372923..c3d50079a 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -1,7 +1,7 @@ import { ComponentMetadata, FeatureBase } from '@/components/component' import { deleteValue } from '@/core/utils' -import { CoreApis } from '../core/core-apis' -import { addData, registerData, registerAndGetData } from './data' +import { CoreApis } from '@/core/core-apis' +import { addData, registerAndGetData, registerData } from './data' import { addHook, getHook } from './hook' /** 插件初始化时的传入参数, 可以解构并调用 */ @@ -48,7 +48,7 @@ export const plugins: PluginMetadata[] = getBuiltInPlugins() /** * 安装插件 - * @param input 插件数据 + * @param code 插件代码 */ export const installPlugin = async (code: string) => { const { parseExternalInput } = await import('../core/external-input') @@ -95,13 +95,9 @@ export const installPlugin = async (code: string) => { */ export const uninstallPlugin = async (nameOrDisplayName: string) => { const { settings } = await import('@/core/settings') - const existingPlugin = Object.entries(settings.userPlugins) - .find(([name, { displayName }]) => { - if (name === nameOrDisplayName || displayName === nameOrDisplayName) { - return true - } - return false - }) + const existingPlugin = Object.entries(settings.userPlugins).find(([name, { displayName }]) => { + return name === nameOrDisplayName || displayName === nameOrDisplayName + }) if (!existingPlugin) { throw new Error(`没有找到与名称'${nameOrDisplayName}'相关联的插件`) } @@ -155,20 +151,29 @@ export const loadPlugin = async (plugin: PluginMetadata) => { * @param components 组件列表 */ export const loadAllPlugins = async (components: ComponentMetadata[]) => { - const { settings, getGeneralSettings } = await import('@/core/settings') - const { batchParseCode } = await import('@/core/external-input') - const otherPlugins = components - .map(extractPluginFromComponent) - .filter(p => p !== null) - .concat(await batchParseCode( + 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!).concat(await loadFeaturesFromCodes( + FeatureKind.Plugin, + Object.keys(settings.userPlugins), Object.values(settings.userPlugins).map(p => p.code), - )) + )).value() plugins.push(...otherPlugins) return Promise.allSettled( plugins.map(loadPlugin), ).then(async () => { if (getGeneralSettings().devMode) { - const { pluginLoadTime, pluginResolveTime } = await import('@/core/performance/plugin-trace') + const { + pluginLoadTime, + pluginResolveTime, + } = await import('@/core/performance/plugin-trace') const { logStats } = await import('@/core/performance/stats') logStats('plugins block', pluginLoadTime) logStats('plugins resolve', pluginResolveTime) From 1be862acba6aa349a132040ba3e2b3cf34f2126d Mon Sep 17 00:00:00 2001 From: timongh <46739861+timongh@users.noreply.github.com> Date: Tue, 29 Mar 2022 15:18:31 +0800 Subject: [PATCH 2/5] Fix default display name --- src/plugins/plugin.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index c3d50079a..2f1d3a08c 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -23,6 +23,7 @@ export interface PluginMinimalData extends FeatureBase { /** 显示名称, 默认同插件名称 */ displayName?: string } + type PartialRequired = Target & { [P in Props]-?: Target[P] } @@ -67,11 +68,12 @@ export const installPlugin = async (code: string) => { message: `已更新插件'${plugin.displayName}', 刷新后生效`, } } - const newPlugin = { - code, - displayName: plugin.name, // 默认等于 name - ...plugin, - } + const newPlugin = lodash(plugin) + .set(['code'], code) + .defaults({ + displayName: plugin.name, // 默认等于 name + }) + .value() settings.userPlugins[plugin.name] = newPlugin plugins.push(newPlugin) // const { coreApis } = await import('../core/core-apis') @@ -159,8 +161,8 @@ export const loadAllPlugins = async (components: ComponentMetadata[]) => { loadFeaturesFromCodes, FeatureKind, } = await import('@/core/external-input/load-features-from-codes') - const otherPlugins = lodash(components).map(extractPluginFromComponent).filter(p => p !== null). - map(p => p!).concat(await loadFeaturesFromCodes( + const otherPlugins = lodash(components).map(extractPluginFromComponent).filter(p => p !== null) + .map(p => p!).concat(await loadFeaturesFromCodes( FeatureKind.Plugin, Object.keys(settings.userPlugins), Object.values(settings.userPlugins).map(p => p.code), From b8ea52e356b02fa85e11eddaf8cc251ea4416985 Mon Sep 17 00:00:00 2001 From: timongh <46739861+timongh@users.noreply.github.com> Date: Tue, 29 Mar 2022 15:37:48 +0800 Subject: [PATCH 3/5] Revert "Fix default display name" This reverts commit 1be862acba6aa349a132040ba3e2b3cf34f2126d. It is not a bug. --- src/plugins/plugin.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index 2f1d3a08c..c3d50079a 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -23,7 +23,6 @@ export interface PluginMinimalData extends FeatureBase { /** 显示名称, 默认同插件名称 */ displayName?: string } - type PartialRequired = Target & { [P in Props]-?: Target[P] } @@ -68,12 +67,11 @@ export const installPlugin = async (code: string) => { message: `已更新插件'${plugin.displayName}', 刷新后生效`, } } - const newPlugin = lodash(plugin) - .set(['code'], code) - .defaults({ - displayName: plugin.name, // 默认等于 name - }) - .value() + const newPlugin = { + code, + displayName: plugin.name, // 默认等于 name + ...plugin, + } settings.userPlugins[plugin.name] = newPlugin plugins.push(newPlugin) // const { coreApis } = await import('../core/core-apis') @@ -161,8 +159,8 @@ export const loadAllPlugins = async (components: ComponentMetadata[]) => { loadFeaturesFromCodes, FeatureKind, } = await import('@/core/external-input/load-features-from-codes') - const otherPlugins = lodash(components).map(extractPluginFromComponent).filter(p => p !== null) - .map(p => p!).concat(await loadFeaturesFromCodes( + const otherPlugins = lodash(components).map(extractPluginFromComponent).filter(p => p !== null). + map(p => p!).concat(await loadFeaturesFromCodes( FeatureKind.Plugin, Object.keys(settings.userPlugins), Object.values(settings.userPlugins).map(p => p.code), From 7cfb9938bafb04226c07714a3bd90fd26e215a20 Mon Sep 17 00:00:00 2001 From: timongh <46739861+timongh@users.noreply.github.com> Date: Thu, 31 Mar 2022 15:53:14 +0800 Subject: [PATCH 4/5] Fix result type of code loading --- .../load-feature-code-all-settled.ts | 22 ++- .../external-input/load-feature-code-all.ts | 158 ++++++++++-------- src/core/external-input/load-feature-code.ts | 110 +++++++----- .../load-features-from-codes.ts | 50 +++--- 4 files changed, 202 insertions(+), 138 deletions(-) diff --git a/src/core/external-input/load-feature-code-all-settled.ts b/src/core/external-input/load-feature-code-all-settled.ts index 71d70b7bd..c036bb494 100644 --- a/src/core/external-input/load-feature-code-all-settled.ts +++ b/src/core/external-input/load-feature-code-all-settled.ts @@ -1,4 +1,6 @@ -import { loadFeatureCode, LoadFeatureCodeResult } from '@/core/external-input/load-feature-code' +import { + loadFeatureCode, LoadFeatureCodeResult, +} from '@/core/external-input/load-feature-code' type LdRes = LoadFeatureCodeResult type SettledRes = PromiseSettledResult @@ -6,11 +8,13 @@ type FilledRes = PromiseFulfilledResult const unwrapSettledResult = (r: SettledRes): T => (r as FilledRes).value -const mapSettledArray = (arr: SettledRes[]): T[] => +const mapSettledArray = (arr: SettledRes[]): T[] => ( arr.map(unwrapSettledResult) +) -const mapSettleResult = (p: Promise[]>): Promise => +const mapSettleResult = (p: Promise[]>): Promise => ( p.then(mapSettledArray) +) /** * 批量加载组件或插件的代码字符串,获取其导出 feature @@ -18,8 +22,10 @@ const mapSettleResult = (p: Promise[]>): Promise => * @param codes 代码字符串数组 * @returns 不会失败的 `Promise`。其结果为一个数组,其中每个元素都是代表代码执行结果的对象 */ -export const loadFeatureCodeAllSettled = (codes: string[]): Promise[]> => lodash(codes) - .map>>(loadFeatureCode) - .thru>[]>>(arr => Promise.allSettled(arr)) - .thru(mapSettleResult) - .value() +export const loadFeatureCodeAllSettled = ( + codes: string[], +): Promise[]> => lodash(codes) + .map>>(loadFeatureCode) + .thru>[]>>(arr => Promise.allSettled(arr)) + .thru(mapSettleResult) + .value() diff --git a/src/core/external-input/load-feature-code-all.ts b/src/core/external-input/load-feature-code-all.ts index 35031b707..4604e774b 100644 --- a/src/core/external-input/load-feature-code-all.ts +++ b/src/core/external-input/load-feature-code-all.ts @@ -1,101 +1,106 @@ import { - loadFeatureCode, - LoadFeatureCodeError, - LoadFeatureCodeResult, + loadFeatureCode, LoadFeatureCodeResult, LoadFeatureCodeResultError, LoadFeatureCodeResultOk, } from '@/core/external-input/load-feature-code' import { FeatureBase } from '@/components/types' -// ========== result type definitions ========== +interface ResultInstance { + readonly isOk: ( + this: LoadFeatureCodeAllResult + ) => this is LoadFeatureCodeAllResultOk -class LoadFeatureCodeAllResultConstructor { - isOk(): this is LoadFeatureCodeAllResultOk { - return this instanceof LoadFeatureCodeAllResultOk - } + readonly isError: ( + this: LoadFeatureCodeAllResult + ) => this is LoadFeatureCodeAllResultError - isError(): this is LoadFeatureCodeAllError { - return !this.isOk() - } + readonly isNoExport: ( + this: LoadFeatureCodeAllResult + ) => this is LoadFeatureCodeAllResultNoExport - isNoExport(): this is LoadFeatureCodeAllResultNoExport { - return this instanceof LoadFeatureCodeAllResultNoExport - } - - isCodeThrew(): this is LoadFeatureCodeAllResultCodeThrew { - return this instanceof LoadFeatureCodeAllResultCodeThrew - } - - tryGetFeatures(): X[] | undefined { - if (this.isOk()) { - return this.features - } - return undefined - } + readonly isCodeThrew: ( + this: LoadFeatureCodeAllResult + ) => this is LoadFeatureCodeAllResultCodeThrew } /** - * 成活从代码中获取 features + * 成功从代码中获取 features * - * @member features 从代码中获取的导出值 + * @namespace + * @property features 从代码中获取的导出值 */ -export class LoadFeatureCodeAllResultOk extends LoadFeatureCodeAllResultConstructor { - constructor(public readonly features: X[]) { - super() - } +interface LoadFeatureCodeAllResultOk extends ResultInstance { + readonly tag: 'Ok', + readonly features: X[], } -/** - * 代码没有导出任何值 - * - * @member index 出错代码对应的代码数组下标 - */ -export class LoadFeatureCodeAllResultNoExport extends LoadFeatureCodeAllResultConstructor { - constructor(public readonly index: number) { - super() - } +/** 代码没有导出任何值 */ +interface LoadFeatureCodeAllResultNoExport extends ResultInstance { + readonly tag: 'NoExport', } /** * 执行代码过程中产生了抛出值。 * - * @member index 出错代码对应的代码数组索引 - * @member thrown 抛出的值 + * @namespace + * @property thrown 抛出的值 */ -export class LoadFeatureCodeAllResultCodeThrew extends LoadFeatureCodeAllResultConstructor { - constructor(public readonly index: number, public readonly thrown: unknown) { - super() - } +interface LoadFeatureCodeAllResultCodeThrew extends ResultInstance { + readonly tag: 'CodeThrew', + readonly thrown: unknown, } -export type LoadFeatureCodeAllError = +type LoadFeatureCodeAllResultError = LoadFeatureCodeAllResultNoExport | LoadFeatureCodeAllResultCodeThrew -export type LoadFeatureCodeAllResult = +type LoadFeatureCodeAllResult = LoadFeatureCodeAllResultOk - | LoadFeatureCodeAllError + | LoadFeatureCodeAllResultError -// ========== internal definitions ========== +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 = ( + features: X[], +): LoadFeatureCodeAllResultOk => 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 = Promise type LdRes = LoadFeatureCodeResult type LdOk = LoadFeatureCodeResultOk -type LdErr = LoadFeatureCodeError +type LdErr = LoadFeatureCodeResultError type LdAllRes = LoadFeatureCodeAllResult type LdAllOk = LoadFeatureCodeAllResultOk -type LdAllErr = LoadFeatureCodeAllError +type LdAllErr = LoadFeatureCodeAllResultError type LoadCodesTask = Task[], [number, LdErr]> -const LdCodeThrew = LoadFeatureCodeAllResultCodeThrew -const LdAllOk = LoadFeatureCodeAllResultOk -const LdAllNoExport = LoadFeatureCodeAllResultNoExport -const LdAllCodeThrew = LoadFeatureCodeAllResultCodeThrew - // covert `Task>` to `Task, LdErr>` const rejectErrorResult = (t: Task>): Task, LdErr> => ( - t.then(r => r.isOk() ? r : Promise.reject(r)) + t.then(r => (r.isOk() ? r : Promise.reject(r))) ) // load feature code, and return `Task, LdErr>` @@ -104,20 +109,22 @@ const loadCode = (code: string): Task, LdErr> => ( ) // covert `Task`'s `Err` type from `T` to `[number, T]` -const addIndexToRejected = (t: Task, i: N): Task => ( - t.catch(e => Promise.reject([i, e])) -) +const addIndexToRejected = ( + t: Task, + i: N, +// eslint-disable-next-line prefer-promise-reject-errors +): Task => t.catch(e => Promise.reject([i, e])) // create `LdAllOk` from an array of `LdOk` const createOkResult = (arr: LdOk[]): LdAllOk => ( - new LdAllOk(arr.map(r => r.feature)) + okResult(arr.map(r => r.feature)) ) // create `LdAllErr` from `[number, LdErr]` const createErrResult = (t: [number, LdErr]): LdAllErr => ( - t[1] instanceof LdCodeThrew - ? new LdAllCodeThrew(t[0], t[1].thrown) - : new LdAllNoExport(t[0]) + t[1].isCodeThrew() + ? codeThrewResult(t[1].thrown) + : noExportResult ) // load all feature codes, and return `LoadCodesTask` @@ -132,8 +139,6 @@ const createTaskResult = (t: LoadCodesTask): Task> => t .then(createOkResult) .catch(createErrResult) -// ========== paramount function to export ========== - /** * 批量加载组件或插件的代码字符串,获取其导出 feature * @@ -142,7 +147,18 @@ const createTaskResult = (t: LoadCodesTask): Task> => t * @param codes 代码字符串数组 * @returns 一个不会失败的 `Promise`,其结果值为 `LoadFeatureCodeAllResult` */ -export const loadFeatureCodeAll = (codes: string[]): Promise> => lodash(codes) - .thru>(loadCodes) - .thru(createTaskResult) - .value() +const loadFeatureCodeAll = (codes: string[]): Promise> => ( + lodash(codes) + .thru>(loadCodes) + .thru(createTaskResult) + .value() +) + +export { + loadFeatureCodeAll, + LoadFeatureCodeAllResult, + LoadFeatureCodeAllResultOk, + LoadFeatureCodeAllResultError, + LoadFeatureCodeAllResultNoExport, + LoadFeatureCodeAllResultCodeThrew, +} diff --git a/src/core/external-input/load-feature-code.ts b/src/core/external-input/load-feature-code.ts index fc8b4778d..0314bff79 100644 --- a/src/core/external-input/load-feature-code.ts +++ b/src/core/external-input/load-feature-code.ts @@ -1,60 +1,83 @@ import { FeatureBase } from '@/components/types' -class LoadFeatureCodeResultConstructor { - isOk(): this is LoadFeatureCodeResultOk { - return this instanceof LoadFeatureCodeResultOk - } +interface ResultInstance { + readonly isOk: ( + this: LoadFeatureCodeResult + ) => this is LoadFeatureCodeResultOk - isError(): this is LoadFeatureCodeError { - return !this.isOk() - } + readonly isError: ( + this: LoadFeatureCodeResult + ) => this is LoadFeatureCodeResultError - isNoExport(): this is LoadFeatureCodeResultNoExport { - return this instanceof LoadFeatureCodeResultNoExport - } + readonly isNoExport: ( + this: LoadFeatureCodeResult + ) => this is LoadFeatureCodeResultNoExport - isCodeThrew(): this is LoadFeatureCodeResultCodeThrew { - return this instanceof LoadFeatureCodeResultCodeThrew - } - - tryGetFeature(): X | undefined { - if (this.isOk()) { - return this.feature - } - return undefined - } + readonly isCodeThrew: ( + this: LoadFeatureCodeResult + ) => this is LoadFeatureCodeResultCodeThrew } /** * 成功从代码中获取 feature * - * @member feature 从代码中获取的导出值 + * @namespace + * @property feature 从代码中获取的导出值 */ -export class LoadFeatureCodeResultOk extends LoadFeatureCodeResultConstructor { - constructor(public readonly feature: X) { - super() - } +interface LoadFeatureCodeResultOk extends ResultInstance { + readonly tag: 'Ok', + readonly feature: X, } /** 代码没有导出任何值 */ -export class LoadFeatureCodeResultNoExport extends LoadFeatureCodeResultConstructor { +interface LoadFeatureCodeResultNoExport extends ResultInstance { + readonly tag: 'NoExport', } /** * 执行代码过程中产生了抛出值。 * - * @member {unknown} thrown 抛出的值 + * @namespace + * @property thrown 抛出的值 */ -export class LoadFeatureCodeResultCodeThrew extends LoadFeatureCodeResultConstructor { - constructor(public readonly thrown: unknown) { - super() - } +interface LoadFeatureCodeResultCodeThrew extends ResultInstance { + readonly tag: 'CodeThrew', + readonly thrown: unknown, } -export type LoadFeatureCodeError = LoadFeatureCodeResultNoExport | LoadFeatureCodeResultCodeThrew -export type LoadFeatureCodeResult = +type LoadFeatureCodeResultError = + LoadFeatureCodeResultNoExport + | LoadFeatureCodeResultCodeThrew +type LoadFeatureCodeResult = LoadFeatureCodeResultOk - | LoadFeatureCodeError + | 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 = (feature: X): LoadFeatureCodeResultOk => 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 @@ -62,17 +85,28 @@ export type LoadFeatureCodeResult = * @param code 代码字符串 * @returns 一个不会失败的 `Promise`,其结果值为 {@link LoadFeatureCodeResult} */ -export const loadFeatureCode = async (code: string): Promise> => { +const loadFeatureCode = async ( + code: string, +): Promise> => { // 收集代码导出值 const exports = {} try { eval(code) } catch (thrown) { - return new LoadFeatureCodeResultCodeThrew(thrown) + return codeThrewResult(thrown) } const values = Object.values(exports) if (values.length === 0) { - return new LoadFeatureCodeResultNoExport() + return noExportResult } - return new LoadFeatureCodeResultOk(values[0] as X) + return okResult(values[0] as X) +} + +export { + loadFeatureCode, + LoadFeatureCodeResult, + LoadFeatureCodeResultOk, + LoadFeatureCodeResultError, + LoadFeatureCodeResultNoExport, + LoadFeatureCodeResultCodeThrew, } diff --git a/src/core/external-input/load-features-from-codes.ts b/src/core/external-input/load-features-from-codes.ts index df1929204..4358a7921 100644 --- a/src/core/external-input/load-features-from-codes.ts +++ b/src/core/external-input/load-features-from-codes.ts @@ -1,33 +1,44 @@ import { - LoadFeatureCodeError, - LoadFeatureCodeResultNoExport, - LoadFeatureCodeResultOk, + LoadFeatureCodeResultError, LoadFeatureCodeResultOk, } from '@/core/external-input/load-feature-code' import { Toast } from '@/core/toast' 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' +import { + loadFeatureCodeAllSettled, +} from '@/core/external-input/load-feature-code-all-settled' -const curConsole = useScopedConsole('@/core/external-input/load-features-from-codes.ts') +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: LoadFeatureCodeError) => void => { +const logError = (kind: FeatureKind): ( + featureName: string, err: LoadFeatureCodeResultError) => void => { const prefix = kind === FeatureKind.Component ? 'component' : 'plugin' return (featureName, err) => { - if (err instanceof LoadFeatureCodeResultNoExport) { - curConsole.error(`${prefix} '${featureName}' exports no value, failed to load`) + 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 }) + curConsole.error( + `${prefix} '${featureName}' throws something when importing, failed to load`, + { thrown: err.thrown }, + ) } } } -const reportErrToUser = (featureKind: FeatureKind, errNames: string[]): void => { +const reportErrToUser = ( + featureKind: FeatureKind, + errNames: string[], +): void => { type ErrInfo = number | string[] const emptyErrInfo: () => string[] = () => [] @@ -37,16 +48,14 @@ const reportErrToUser = (featureKind: FeatureKind, errNames: string[]): void => if (acc.length < 3) { acc.push(featureName) return acc - } else { - return 4 } - } else { - return acc + 1 + return 4 } + return acc + 1 } - const reportErrInfo = (featureKind: FeatureKind, info: ErrInfo): void => { - const kindName = featureKind === FeatureKind.Component ? '组件' : '插件' + const reportErrInfo = (kind: FeatureKind, info: ErrInfo): void => { + const kindName = kind === FeatureKind.Component ? '组件' : '插件' if (Array.isArray(info)) { Toast.error( `${kindName} "${info.join('", "')}" 加载失败。请向我们反馈,以解决此问题。`, @@ -64,8 +73,7 @@ const reportErrToUser = (featureKind: FeatureKind, errNames: string[]): void => reportErrInfo(featureKind, errInfo) } -type KindToType = - K extends FeatureKind.Component ? ComponentMetadata : PluginMetadata +export type FeatureMetadata = ComponentMetadata | PluginMetadata /** * 批量加载组件或插件代码 @@ -93,8 +101,8 @@ export async function loadFeaturesFromCodes( kind: FeatureKind, names: string[], codes: string[], -): Promise<(ComponentMetadata | PluginMetadata)[]> { - const results = await loadFeatureCodeAllSettled>(codes) +): Promise { + const results = await loadFeatureCodeAllSettled(codes) const [namedOk, namedErr] = lodash(results) .map((r, i) => [names[i], r] as const) .partition(([, r]) => r.isOk()) @@ -111,6 +119,6 @@ export async function loadFeaturesFromCodes( return lodash.map( namedOk, - ([, r]) => (r as LoadFeatureCodeResultOk>).feature, + ([, r]) => (r as LoadFeatureCodeResultOk).feature, ) } From 3d155b33305c736ff7f6db195afdd3650560bb88 Mon Sep 17 00:00:00 2001 From: timongh <46739861+timongh@users.noreply.github.com> Date: Thu, 31 Mar 2022 15:54:34 +0800 Subject: [PATCH 5/5] Change null assert to 'as' --- src/plugins/plugin.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index c3d50079a..2dc984e99 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -23,6 +23,7 @@ export interface PluginMinimalData extends FeatureBase { /** 显示名称, 默认同插件名称 */ displayName?: string } + type PartialRequired = Target & { [P in Props]-?: Target[P] } @@ -95,9 +96,11 @@ export const installPlugin = async (code: string) => { */ export const uninstallPlugin = async (nameOrDisplayName: string) => { const { settings } = await import('@/core/settings') - const existingPlugin = Object.entries(settings.userPlugins).find(([name, { displayName }]) => { - return name === nameOrDisplayName || displayName === nameOrDisplayName - }) + const existingPlugin = Object.entries(settings.userPlugins).find( + ([name, { displayName }]) => ( + name === nameOrDisplayName || displayName === nameOrDisplayName + ), + ) if (!existingPlugin) { throw new Error(`没有找到与名称'${nameOrDisplayName}'相关联的插件`) } @@ -159,12 +162,16 @@ export const loadAllPlugins = async (components: ComponentMetadata[]) => { loadFeaturesFromCodes, FeatureKind, } = await import('@/core/external-input/load-features-from-codes') - const otherPlugins = lodash(components).map(extractPluginFromComponent).filter(p => p !== null). - map(p => p!).concat(await loadFeaturesFromCodes( + 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() + )) + .value() plugins.push(...otherPlugins) return Promise.allSettled( plugins.map(loadPlugin),