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 01/48] 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') From 885b00d6a87679c320525e68c757425f0b44858a Mon Sep 17 00:00:00 2001 From: timongh <46739861+timongh@users.noreply.github.com> Date: Thu, 19 Jan 2023 22:41:28 +0800 Subject: [PATCH 02/48] Replace all `parseExternalInput` to `loadFeatureCode` --- src/components/user-component.ts | 8 +++++--- src/core/external-input/index.ts | 2 ++ src/core/install-feature.ts | 4 ++-- src/core/version.ts | 4 ++-- src/plugins/plugin.ts | 8 +++++--- src/plugins/style.ts | 4 ++-- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/components/user-component.ts b/src/components/user-component.ts index d2c81f969..ceaeec841 100644 --- a/src/components/user-component.ts +++ b/src/components/user-component.ts @@ -8,9 +8,11 @@ import { ComponentMetadata, componentsMap } from './component' */ export const installComponent = async (code: string) => { const { components } = await import('./component') - const { parseExternalInput } = await import('../core/external-input') - const component = await parseExternalInput(code) - if (component === null) { + const { loadFeatureCode } = await import('@/core/external-input') + let component: ComponentMetadata + try { + component = loadFeatureCode(code) as ComponentMetadata + } catch { throw new Error('无效的组件代码') } const { settings } = await import('@/core/settings') diff --git a/src/core/external-input/index.ts b/src/core/external-input/index.ts index 1a56cd3c1..653c992b1 100644 --- a/src/core/external-input/index.ts +++ b/src/core/external-input/index.ts @@ -1,5 +1,7 @@ import { pickFile } from '@/core/file-picker' +export * from './load-feature-code' + /** 外部输入包装类型, 详见`parseExternalInput`的文档 */ export type ExternalInput = undefined | string | T // const arrayReturn = ( diff --git a/src/core/install-feature.ts b/src/core/install-feature.ts index 1bb85a519..8ac6280a0 100644 --- a/src/core/install-feature.ts +++ b/src/core/install-feature.ts @@ -36,8 +36,8 @@ export const installFeatureFromCode = async ( metadata: FeatureType message: string }> => { - const { parseExternalInput } = await import('../core/external-input') - const item = await parseExternalInput(code) + const { loadFeatureCode } = await import('../core/external-input') + const item = loadFeatureCode(code) const { type, installer } = (() => { if (isComponent(item)) { return { diff --git a/src/core/version.ts b/src/core/version.ts index 3f19c1fc5..795dd4c42 100644 --- a/src/core/version.ts +++ b/src/core/version.ts @@ -1,5 +1,5 @@ import { FeatureBase } from '@/components/types' -import { parseExternalInput } from './external-input' +import { loadFeatureCode } from './external-input' import { meta } from './meta' export enum CompareResult { @@ -51,7 +51,7 @@ export class Version { export const isFeatureAcceptable = async (feature: FeatureBase | string) => { try { if (typeof feature === 'string') { - feature = await parseExternalInput(feature) + feature = loadFeatureCode(feature) as FeatureBase } // 无效代码 if (feature === null || feature === undefined) { diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index ba58ceb78..fb31ebd03 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -54,9 +54,11 @@ export const plugins: PluginMetadata[] = getBuiltInPlugins() * @param code 插件代码 */ export const installPlugin = async (code: string) => { - const { parseExternalInput } = await import('../core/external-input') - const plugin = await parseExternalInput(code) - if (plugin === null) { + const { loadFeatureCode } = await import('@/core/external-input') + let plugin: PluginMetadata + try { + plugin = loadFeatureCode(code) as PluginMetadata + } catch { throw new Error('无效的插件代码') } const { settings } = await import('@/core/settings') diff --git a/src/plugins/style.ts b/src/plugins/style.ts index 4888058ee..769cd1c7a 100644 --- a/src/plugins/style.ts +++ b/src/plugins/style.ts @@ -29,9 +29,9 @@ export const styles: Required[] = Object.values(settings.userStyles) export const installStyle = async (input: UserStyle | string) => { try { let userStyle: UserStyle - const { parseExternalInput } = await import('../core/external-input') + const { loadFeatureCode } = await import('@/core/external-input') if (typeof input === 'string') { - userStyle = await parseExternalInput(input) + userStyle = loadFeatureCode(input) as UserStyle } else { userStyle = input } From ce88b1e314954676737cd4ced39ab3bf4d58a6f0 Mon Sep 17 00:00:00 2001 From: timongh <46739861+timongh@users.noreply.github.com> Date: Thu, 19 Jan 2023 22:52:06 +0800 Subject: [PATCH 03/48] Change the return type of `loadFeatrueCode` to `unknown` --- src/core/external-input/load-feature-code.ts | 4 ++-- src/core/install-feature.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/external-input/load-feature-code.ts b/src/core/external-input/load-feature-code.ts index 9d5db0532..4e330e7f0 100644 --- a/src/core/external-input/load-feature-code.ts +++ b/src/core/external-input/load-feature-code.ts @@ -13,10 +13,10 @@ export class LoadFeatureError extends Error {} * 2. 若代码整体为一个表达式,则导出表达式的返回值 * * @param code - 被执行的代码 - * @returns 导出的元数据 + * @returns 导出的元数据(不检测正确性) * @throws {@link LoadFeatureError} 代码抛出了一个值或代码存在语法错误 */ -export const loadFeatureCode = (code: string): ComponentMetadata | PluginMetadata | UserStyle => { +export const loadFeatureCode = (code: string): unknown => { // 将 `key` 和 `val` 临时赋值到 `target` 上并返回 // 调用返回值中的 restore 函数,可以恢复 `target` 中该属性的原始情况(包括属性不存在的情况) const temporarilySet = ( diff --git a/src/core/install-feature.ts b/src/core/install-feature.ts index 8ac6280a0..bfb6bc690 100644 --- a/src/core/install-feature.ts +++ b/src/core/install-feature.ts @@ -37,7 +37,7 @@ export const installFeatureFromCode = async ( message: string }> => { const { loadFeatureCode } = await import('../core/external-input') - const item = loadFeatureCode(code) + const item = loadFeatureCode(code) as FeatureType const { type, installer } = (() => { if (isComponent(item)) { return { From 3e84e9b37c0b5bc73faf2211c4caa553caa72b55 Mon Sep 17 00:00:00 2001 From: timongh <46739861+timongh@users.noreply.github.com> Date: Fri, 20 Jan 2023 18:07:43 +0800 Subject: [PATCH 04/48] Optimize `loadFeatureCode` Add a sandbox for code execution. --- src/core/external-input/load-feature-code.ts | 126 ++++++++++--------- 1 file changed, 69 insertions(+), 57 deletions(-) diff --git a/src/core/external-input/load-feature-code.ts b/src/core/external-input/load-feature-code.ts index 4e330e7f0..7e926330a 100644 --- a/src/core/external-input/load-feature-code.ts +++ b/src/core/external-input/load-feature-code.ts @@ -1,70 +1,82 @@ -import { ComponentMetadata } from '@/components/types' -import { PluginMetadata } from '@/plugins/plugin' -import { UserStyle } from '@/plugins/style' +export class LoadFeatureCodeError extends Error {} -export class LoadFeatureError extends Error {} +interface CodeSandbox { + /** + * 在沙箱中执行代码 + * + * @remarks + * 代码执行时的相关注意事项见 {@link loadFeatureCode} + * + * @returns 一个二元组:`[导出值, 返回值]` + * @throws {@link LoadFeatureCodeError} + * 代码包含语法错误或代码执行时产生了异常 + */ + run(code: string): [unknown, unknown] +} + +/** + * 获取代码运行沙箱 + * + * @returns 一个函数:接受代码,返回。 + */ +const getSandbox = lodash.once((): CodeSandbox => { + // 需要被注入到 `sandbox` 中的键值对 + const injection = new Map([ + // 加固,防止逃逸 + [Symbol.unscopables, undefined], + ['unsafeWindow', unsafeWindow], + ] as [keyof any, unknown][]) + // 目标代码执行时的全局对象代理 + const sandbox = new Proxy(Object.create(null), { + has: () => true, + get: (_, p) => (injection.has(p) ? injection.get(p) : window[p as string]), + set: (_, p, v) => !injection.has(p) && (window[p as string] = v), + }) + const codeKey = 'BILIBILI_EVOLVED_LOAD_FEATURE_CODE_CODE_KEY_3B63D912__' + // eslint-disable-next-line no-new-func + const fn = Function( + 'window', + `with (window) { + return eval(${codeKey}) + }`, + ).bind(undefined, sandbox) + return { + run(code) { + injection.set('exports', {}) + injection.set(codeKey, code) + let returned + try { + returned = fn() + } catch (e) { + throw new LoadFeatureCodeError(undefined, { cause: e }) + } + const exportsValues = Object.values(injection.get('exports')) + const exported = exportsValues.length > 0 ? exportsValues[0] : undefined + return [exported, returned] + }, + } +}) /** * 执行 feature (component, plugin, style) 的代码,并尝试获取其导出元数据 * * @remarks - * feature 代码支持两种导出格式: - * 1. 在本项目中打包 feature 所使用的导出格式 + * feature 代码支持两种导出方式: + * 1. 以 UMD 方式打包的库 * 2. 若代码整体为一个表达式,则导出表达式的返回值 * + * 该函数线程不安全 + * + * 代码默认以非严格模式执行,启用需自行添加 `use strict`。(从本项目中打包的 feature 自带严格模式) + * + * 全局对象为脚本管理器提供的 `window`,支持访问 `unsafeWindow`。 + * * @param code - 被执行的代码 * @returns 导出的元数据(不检测正确性) - * @throws {@link LoadFeatureError} 代码抛出了一个值或代码存在语法错误 + * @throws {@link LoadFeatureCodeError} + * 代码包含语法错误或代码执行时产生了异常 */ export const loadFeatureCode = (code: string): unknown => { - // 将 `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, - } - } - - // 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 + const [exported, returned] = getSandbox().run(code) + return exported || returned } From 6e5fb51d84cbc152938876a073acb6df96582f76 Mon Sep 17 00:00:00 2001 From: timongh <46739861+timongh@users.noreply.github.com> Date: Fri, 20 Jan 2023 18:16:40 +0800 Subject: [PATCH 05/48] Add more error messages for parts of the code that use `loadFeatureCode` --- src/components/component.ts | 9 +++++---- src/components/user-component.ts | 4 ++-- src/plugins/plugin.ts | 13 +++++++------ src/plugins/style.ts | 4 ++-- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/component.ts b/src/components/component.ts index 6da349d93..9ef3d8115 100644 --- a/src/components/component.ts +++ b/src/components/component.ts @@ -153,10 +153,11 @@ export const loadAllUserComponents = async () => { let metadata: ComponentMetadata try { metadata = loadFeatureCode(code) as ComponentMetadata - } catch { - console.error( - `从代码加载用户组件失败。代码可能有语法错误或代码执行时有抛出值。组件名:'${name}'`, - ) + } catch (e) { + console.error('从代码加载用户组件失败。代码可能有语法错误或代码执行时有抛出值。', { + componentName: name, + error: e, + }) continue } loadUserComponent(metadata) diff --git a/src/components/user-component.ts b/src/components/user-component.ts index ceaeec841..b29b1ceac 100644 --- a/src/components/user-component.ts +++ b/src/components/user-component.ts @@ -12,8 +12,8 @@ export const installComponent = async (code: string) => { let component: ComponentMetadata try { component = loadFeatureCode(code) as ComponentMetadata - } catch { - throw new Error('无效的组件代码') + } catch (e) { + throw new Error('无效的组件代码', { cause: e }) } const { settings } = await import('@/core/settings') if (isBuiltInComponent(component.name)) { diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index fb31ebd03..d93ea2859 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -58,8 +58,8 @@ export const installPlugin = async (code: string) => { let plugin: PluginMetadata try { plugin = loadFeatureCode(code) as PluginMetadata - } catch { - throw new Error('无效的插件代码') + } catch (e) { + throw new Error('无效的插件代码', e) } const { settings } = await import('@/core/settings') const existingPlugin = settings.userPlugins[plugin.name] @@ -169,10 +169,11 @@ export const loadAllPlugins = async (components: ComponentMetadata[]) => { let metadata: PluginMetadata try { metadata = loadFeatureCode(code) as PluginMetadata - } catch { - console.error( - `从代码加载用户插件失败。代码可能有语法错误或代码执行时有抛出值。插件名:'${name}'`, - ) + } catch (e) { + console.error('从代码加载用户插件失败。代码可能包含语法错误或执行时产生了异常', { + pluginName: name, + error: e, + }) continue } plugins.push(metadata) diff --git a/src/plugins/style.ts b/src/plugins/style.ts index 769cd1c7a..a4cadfcc9 100644 --- a/src/plugins/style.ts +++ b/src/plugins/style.ts @@ -59,8 +59,8 @@ export const installStyle = async (input: UserStyle | string) => { metadata: userStyle, message: `已安装样式'${displayName || name}'`, } - } catch (error) { - throw new Error('无效的样式代码') + } catch (e) { + throw new Error('无效的样式代码', { cause: e }) } } /** From 80794de10dd8c337e10806d1195bc204b85c5a6e Mon Sep 17 00:00:00 2001 From: the1812 Date: Mon, 23 Jan 2023 21:40:45 +0800 Subject: [PATCH 06/48] Fix style for gift panel (fix #3243) --- .../components/style/simplify/live/live.scss | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/registry/lib/components/style/simplify/live/live.scss b/registry/lib/components/style/simplify/live/live.scss index b7d9759a1..41f5efd89 100644 --- a/registry/lib/components/style/simplify/live/live.scss +++ b/registry/lib/components/style/simplify/live/live.scss @@ -1,4 +1,4 @@ -$prefix: "simplifyLiveroom-switch"; +$prefix: 'simplifyLiveroom-switch'; .#{$prefix} { &-vip .vip-icon, &-fansMedal .fans-medal-item-ctnr, @@ -175,16 +175,14 @@ $prefix: "simplifyLiveroom-switch"; } .chat-history-panel, #pay-note-panel-vm .pay-note-panel, - #pay-note-panel-vm .pay-note-panel .detail-info .mask - { + #pay-note-panel-vm .pay-note-panel .detail-info .mask { border-radius: 11px 11px 0 0 !important; } } &.player-full-win { .chat-history-panel, #pay-note-panel-vm .pay-note-panel, - #pay-note-panel-vm .pay-note-panel .detail-info .mask - { + #pay-note-panel-vm .pay-note-panel .detail-info .mask { border-radius: 0 !important; } } @@ -202,6 +200,25 @@ $prefix: "simplifyLiveroom-switch"; .gift-control-panel { height: 48px !important; } + .gift-left-part { + display: flex !important; + align-items: center !important; + gap: 6px !important; + width: auto !important; + height: auto !important; + padding: 10px 0 0 16px !important; + .entry-icon { + margin: 0 !important; + width: 24px !important; + height: 24px !important; + } + .wait-num { + display: none !important; + } + .time-span { + margin: 1px 0 0 0 !important; + } + } .draw-box, .anchor-lottery-entry, .treasure-box { @@ -277,7 +294,7 @@ $prefix: "simplifyLiveroom-switch"; background-image: none !important; } } - .chat-history-panel [class*="guard-level-"] { + .chat-history-panel [class*='guard-level-'] { padding: 4px 5px !important; margin: 0 !important; &::after { From f408fb1fdaf7b452b3cd82ab67daa9bae10f2122 Mon Sep 17 00:00:00 2001 From: timongh <46739861+timongh@users.noreply.github.com> Date: Sun, 29 Jan 2023 11:39:46 +0800 Subject: [PATCH 07/48] Fix lint errors/warnings --- registry/lib/components/utils/import-series/logic.ts | 6 +++--- .../components/video/player/extend-speed/component.ts | 1 + src/components/i18n/dom-translator.ts | 10 ++++++++++ src/core/utils/index.ts | 1 + 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/registry/lib/components/utils/import-series/logic.ts b/registry/lib/components/utils/import-series/logic.ts index ac4786738..cf9f4cc6b 100644 --- a/registry/lib/components/utils/import-series/logic.ts +++ b/registry/lib/components/utils/import-series/logic.ts @@ -19,7 +19,7 @@ const importSeries = async (sid, uid, csrf) => { // 创建收藏夹,获取新收藏夹id let favId = 0 - while (true) { + for (;;) { const response = await fetch('https://api.bilibili.com/x/v3/fav/folder/add', { method: 'POST', credentials: 'include', @@ -43,7 +43,7 @@ const importSeries = async (sid, uid, csrf) => { for (let i = 0; i < seriesVideos.length; i++) { // 做个延迟,防止太快而遭服务器拒绝 await delay(500) - while (true) { + for (;;) { const response = await fetch('https://api.bilibili.com/x/v3/fav/resource/deal', { method: 'POST', credentials: 'include', @@ -72,7 +72,7 @@ const importSeries = async (sid, uid, csrf) => { } const importCollection = async (sid, csrf) => { - while (true) { + for (;;) { const response = await fetch('https://api.bilibili.com/x/v3/fav/season/fav', { method: 'POST', credentials: 'include', diff --git a/registry/lib/components/video/player/extend-speed/component.ts b/registry/lib/components/video/player/extend-speed/component.ts index d4bc9c82c..adf92751a 100644 --- a/registry/lib/components/video/player/extend-speed/component.ts +++ b/registry/lib/components/video/player/extend-speed/component.ts @@ -444,6 +444,7 @@ export class ExtendSpeedComponent extends EntrySpeedComponent { setTimeout(() => this.forceUpdateStyle(value)) } + // eslint-disable-next-line class-methods-use-this protected readonly filterNativeSpeed = () => ({ subscribe, next }: PublishContext) => { diff --git a/src/components/i18n/dom-translator.ts b/src/components/i18n/dom-translator.ts index 57d8c5675..9b95f4ec8 100644 --- a/src/components/i18n/dom-translator.ts +++ b/src/components/i18n/dom-translator.ts @@ -12,11 +12,15 @@ export class Translator { static map: Map static regex: [RegExp, string][] + // eslint-disable-next-line class-methods-use-this protected accepts = (node: Node) => node.nodeType === Node.ELEMENT_NODE + // eslint-disable-next-line class-methods-use-this protected getValue = (node: Node) => node.nodeValue + // eslint-disable-next-line class-methods-use-this protected setValue = (node: Node, value: string) => { node.nodeValue = value } + // eslint-disable-next-line class-methods-use-this protected getElement = (node: Node) => node as Element translate(node: Node) { let value = this.getValue(node) @@ -99,17 +103,23 @@ export class Translator { } } export class TextNodeTranslator extends Translator { + // eslint-disable-next-line class-methods-use-this accepts = (node: Node) => node.nodeType === Node.TEXT_NODE + // eslint-disable-next-line class-methods-use-this getElement = (node: Node) => node.parentElement } export class TitleTranslator extends Translator { + // eslint-disable-next-line class-methods-use-this getValue = (node: Node) => (node as Element).getAttribute('title') + // eslint-disable-next-line class-methods-use-this setValue = (node: Node, value: string) => { ;(node as Element).setAttribute('title', value) } } export class PlaceholderTranslator extends Translator { + // eslint-disable-next-line class-methods-use-this getValue = (node: Node) => (node as Element).getAttribute('placeholder') + // eslint-disable-next-line class-methods-use-this setValue = (node: Node, value: string) => { ;(node as Element).setAttribute('placeholder', value) } diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 270e5e04d..babc4e035 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -407,6 +407,7 @@ export class DoubleClickEvent { singleClickHandler: (e: MouseEvent) => void = none private clickedOnce = false + // eslint-disable-next-line class-methods-use-this private readonly stopPropagationHandler = (e: MouseEvent) => { e.stopImmediatePropagation() } From 617937ccb0c79a55cfa112496dd2b0db61ca90cf Mon Sep 17 00:00:00 2001 From: the1812 Date: Wed, 1 Feb 2023 22:16:43 +0800 Subject: [PATCH 08/48] Remove template class (fix #3951) --- registry/lib/components/utils/dev-client/Widget.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/lib/components/utils/dev-client/Widget.vue b/registry/lib/components/utils/dev-client/Widget.vue index 589c1dd58..b48e90862 100644 --- a/registry/lib/components/utils/dev-client/Widget.vue +++ b/registry/lib/components/utils/dev-client/Widget.vue @@ -10,7 +10,7 @@ 断开连接 -