From c6d7d74a94acec6467cd236ea64f5553630256ac Mon Sep 17 00:00:00 2001 From: timongh <46739861+timongh@users.noreply.github.com> Date: Thu, 19 Jan 2023 22:19:36 +0800 Subject: [PATCH] Refactoring how feature code is loaded --- src/components/component.ts | 25 ++- .../load-feature-code-all-settled.ts | 26 --- .../external-input/load-feature-code-all.ts | 156 ---------------- src/core/external-input/load-feature-code.ts | 166 +++++++----------- .../load-features-from-codes.ts | 114 ------------ src/plugins/plugin.ts | 36 ++-- 6 files changed, 96 insertions(+), 427 deletions(-) delete mode 100644 src/core/external-input/load-feature-code-all-settled.ts delete mode 100644 src/core/external-input/load-feature-code-all.ts delete mode 100644 src/core/external-input/load-features-from-codes.ts diff --git a/src/components/component.ts b/src/components/component.ts index 7e589aa80..6da349d93 100644 --- a/src/components/component.ts +++ b/src/components/component.ts @@ -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 () => { diff --git a/src/core/external-input/load-feature-code-all-settled.ts b/src/core/external-input/load-feature-code-all-settled.ts deleted file mode 100644 index f66075b74..000000000 --- a/src/core/external-input/load-feature-code-all-settled.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 24d7a69e5..000000000 --- a/src/core/external-input/load-feature-code-all.ts +++ /dev/null @@ -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: ( - this: LoadFeatureCodeAllResult, - ) => this is LoadFeatureCodeAllResultOk - - readonly isError: ( - this: LoadFeatureCodeAllResult, - ) => this is LoadFeatureCodeAllResultError - - readonly isNoExport: ( - this: LoadFeatureCodeAllResult, - ) => this is LoadFeatureCodeAllResultNoExport - - readonly isCodeThrew: ( - this: LoadFeatureCodeAllResult, - ) => this is LoadFeatureCodeAllResultCodeThrew -} - -/** - * 成功从代码中获取 features - * - * @namespace - * @property features 从代码中获取的导出值 - */ -interface LoadFeatureCodeAllResultOk 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 = - | LoadFeatureCodeAllResultOk - | 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 = (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 = LoadFeatureCodeResultError - -type LdAllRes = LoadFeatureCodeAllResult -type LdAllOk = LoadFeatureCodeAllResultOk -type LdAllErr = LoadFeatureCodeAllResultError - -type LoadCodesTask = Task[], [number, LdErr]> - -// 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, - // 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 => 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 = (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) - -/** - * 批量加载组件或插件的代码字符串,获取其导出 feature - * - * 只要有一个代码出现了错误,则返回错误。 - * - * @param codes 代码字符串数组 - * @returns 一个不会失败的 `Promise`,其结果值为 `LoadFeatureCodeAllResult` - */ -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 a3e1fb9dc..9d5db0532 100644 --- a/src/core/external-input/load-feature-code.ts +++ b/src/core/external-input/load-feature-code.ts @@ -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: ( - this: LoadFeatureCodeResult, - ) => this is LoadFeatureCodeResultOk - - readonly isError: (this: LoadFeatureCodeResult) => this is LoadFeatureCodeResultError - - readonly isNoExport: ( - this: LoadFeatureCodeResult, - ) => this is LoadFeatureCodeResultNoExport - - readonly isCodeThrew: ( - this: LoadFeatureCodeResult, - ) => this is LoadFeatureCodeResultCodeThrew -} +export class LoadFeatureError extends Error {} /** - * 成功从代码中获取 feature + * 执行 feature (component, plugin, style) 的代码,并尝试获取其导出元数据 * - * @namespace - * @property feature 从代码中获取的导出值 - */ -interface LoadFeatureCodeResultOk 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 = - | LoadFeatureCodeResultOk - | 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 - * - * @param code 代码字符串 - * @returns 一个不会失败的 `Promise`,其结果值为 {@link LoadFeatureCodeResult} - */ -const loadFeatureCode = async ( - code: string, -): Promise> => { - // 收集代码导出值 - 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 = ( + target: O, + key: K, + val: V, + ): { target: O & Record; 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, + 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 } diff --git a/src/core/external-input/load-features-from-codes.ts b/src/core/external-input/load-features-from-codes.ts deleted file mode 100644 index 400da78de..000000000 --- a/src/core/external-input/load-features-from-codes.ts +++ /dev/null @@ -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 -export async function loadFeaturesFromCodes( - kind: FeatureKind.Plugin, - names: string[], - codes: string[], -): Promise -export async function loadFeaturesFromCodes( - kind: FeatureKind, - names: string[], - codes: string[], -): Promise { - 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 62e690a87..ba58ceb78 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -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')