From 439527f2e33d44cbe8ba7dc84350af2263c6e86e Mon Sep 17 00:00:00 2001 From: FurryR Date: Sun, 29 Oct 2023 19:47:23 +0800 Subject: [PATCH 1/3] :sparkles: A little refactor --- .prettierrc.json | 10 + README-ja_JP.md | 55 ++++ README.md | 2 +- package.json | 2 + src/frontend/index.ts | 92 ++++-- src/global.d.ts | 15 +- src/index.ts | 7 +- src/injector/inject.ts | 259 ++++++++------- src/l10n/lang.ts | 60 ++++ src/loader/dispatch/central-dispatch.ts | 65 ++-- src/loader/dispatch/shared-dispatch.ts | 120 +++++-- src/loader/dispatch/worker-dispatch.ts | 49 +-- src/loader/loader.ts | 404 +++++++++++++++--------- src/loader/make-ctx.ts | 40 ++- src/loader/sandbox.worker.ts | 35 +- src/typings/argument-type.ts | 6 +- src/typings/block-type.ts | 6 +- src/typings/extension-metadata.ts | 70 ++-- src/typings/reporter-scope.ts | 6 +- src/typings/target-type.ts | 6 +- src/util/cast.ts | 45 +-- src/util/color.ts | 130 ++++---- src/util/log.ts | 38 ++- src/util/maybe-format-message.ts | 6 +- webpack.config.js | 2 +- yarn.lock | 5 + 26 files changed, 981 insertions(+), 554 deletions(-) create mode 100644 .prettierrc.json create mode 100644 README-ja_JP.md create mode 100644 src/l10n/lang.ts diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..2f3f1d9 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "semi": true, + "tabWidth": 4, + "singleQuote": true, + "trailingComma": "es5", + "arrowParens": "always", + "bracketSameLine": true, + "bracketSpacing": true, + "printWidth": 80 +} \ No newline at end of file diff --git a/README-ja_JP.md b/README-ja_JP.md new file mode 100644 index 0000000..a636a48 --- /dev/null +++ b/README-ja_JP.md @@ -0,0 +1,55 @@ +
+ +logo + +# Chibi (チビ) + +#### とあるユニバーサル Scratch 拡張機能ローダーです。 + +
+ +--- + +Chibi は Tampermonkey/GreasyMonkey に対応するユーザースクリプトであり、「どのエディターでも拡張機能が使える」という仕様が搭載されています(理論的に)。 + +# ✨ 仕様 + +- [X] 標準 Scratch 拡張機能をロードできる +- [X] Unsandboxed 拡張機能をロードできる +- [X] どこでも Turbowarp 専用機能を使える (一部) +- [ ] 拡張機能をインストールしなくでもサードパーティー拡張機能を搭載したプロジェクトを起動できる +- [X] 直接的にエディターから拡張機能をロードできる + +# 🌈 プラットフォーム + +- [X] Scratch +- [X] CodingClip +- [X] Cocrea +- [X] Aerfaying +- [X] Cocrea 中国版 +- [X] XMW +- [X] CodeLab +- [X] 40code +- [X] TurboWarp + +# 🔥 使い方 + +1. Tampermonkey / Greasymonkey をインストールします。 +2. [リリースページ](https://github.com/SimonShiki/chibi/releases)を開き、バージョンを選択してインストールします。 +3. 'ブロック定義'のカテゴリーで、'Open Frontend' が現れます。クリックすると拡張機能をサイドロードできます。(都合により、およそ5秒のラグがあります) + +> うん…エディターによって、ボタンが正常に現れない場合もあります。その時は DevTools で拡張機能をロードできます。 + +1. 'Ctrl + Shift + J' (Windows) / 'Cmd + Opt + J' (MacOS) で DevTools を開きます。 +2. コンソールで ``chibi.openFrontend()`` や ``chibi.loader.load([extensionURL], [load mode, like 'unsandboxed'])'``を実行します。 +3. これで完成。 + +# 🥰 拡張機能の投稿 + +Chibi のダッシュボードには拡張機能のギャラリーがあります。どうぞ自由に好きな拡張機能を使ってください。 + +もし良かったら [moth](https://github.com/SimonShiki/moth) で自分の一番好きな拡張機能を投稿してください。 + +# ⚓ ライセンス + +AGPL-3.0 です。詳しくは[こちら](./LICENSE)。 diff --git a/README.md b/README.md index 534fd3b..2085f7b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[简体中文](./README-zh_CN.md) +[简体中文](./README-zh_CN.md) [日本語](./README-ja_JP.md)
diff --git a/package.json b/package.json index bb7cc13..d7e6dcf 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "scripts": { "build": "NODE_ENV=production webpack --color --bail", "lint": "eslint ./src/ --ext .js,.ts,tsx,jsx", + "format": "prettier -w --config .prettierrc.json ./src", "typecheck": "tsc --watch --noEmit" }, "devDependencies": { @@ -23,6 +24,7 @@ "eslint": "^8.49.0", "html-webpack-plugin": "^5.5.3", "mini-svg-data-uri": "^1.4.4", + "prettier": "^3.0.3", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1", diff --git a/src/frontend/index.ts b/src/frontend/index.ts index 7edf158..b3b3256 100644 --- a/src/frontend/index.ts +++ b/src/frontend/index.ts @@ -10,7 +10,7 @@ interface MothDispatchedLoad { info: { url: string; sandboxed: boolean; - } + }; } interface MothDispatchedAllocate { @@ -19,49 +19,81 @@ interface MothDispatchedAllocate { type MothDispatched = MothDispatchedAllocate | MothDispatchedLoad; -function getExtensionInfo () { +/** + * Get all extensions. + * @returns Extensions. + */ +function getExtensionInfo() { const processedExtInfo: MothExtensionInfo[] = []; - for (const [extId, ext] of window.chibi.loader.loadedScratchExtension.entries()) { + for (const [ + extId, + ext, + ] of window.chibi.loader.loadedScratchExtension.entries()) { processedExtInfo.push({ name: extId, - sandboxed: ext.env === 'sandboxed' + sandboxed: ext.env === 'sandboxed', }); } return processedExtInfo; } - -async function messageHandler (event: MessageEvent) { +/** + * Handle messages from the frontend (popup window). + * @param event Event from the frontend. + */ +async function messageHandler(event: MessageEvent) { if (event.origin !== 'https://chibi.codingclip.cc') return; if (!('type' in event.data)) return; switch ((event.data as MothDispatched).type) { - case 'allocate': - console.log('handshake with frontend'); - dashboardWindow?.postMessage({ - type: 'handshake', - clientInfo: { - version: Number(window.chibi.version), - url: window.location.host - } - }, '*'); - dashboardWindow?.postMessage({ - type: 'extension', - extensions: getExtensionInfo() - }, '*'); - break; - case 'load': - await window.chibi.loader.load(event.data.info.url, event.data.info.sandboxed ? 'sandboxed' : 'unsandboxed'); - dashboardWindow?.postMessage({ - type: 'extension', - extensions: getExtensionInfo() - }, '*'); - break; + case 'allocate': + // Handshake: send current extension info in order to prepare frontend. + console.log('handshake with frontend'); + dashboardWindow?.postMessage( + { + type: 'handshake', + clientInfo: { + version: Number(window.chibi.version), + url: window.location.host, + }, + }, + '*' + ); + dashboardWindow?.postMessage( + { + type: 'extension', + extensions: getExtensionInfo(), + }, + '*' + ); + break; + case 'load': + // Load a extension. + await window.chibi.loader.load( + event.data.info.url, + event.data.info.sandboxed ? 'sandboxed' : 'unsandboxed' + ); + dashboardWindow?.postMessage( + { + type: 'extension', + extensions: getExtensionInfo(), + }, + '*' + ); + break; } } +// Here we add a listener to process the message window.addEventListener('message', messageHandler); -function openFrontend () { - dashboardWindow = window.open('https://chibi.codingclip.cc/#manage'); +/** + * Open the popup (?) window. + * @param open window.open function (compatible with ccw). + */ +function openFrontend(open: typeof window.open) { + dashboardWindow = open( + 'https://chibi.codingclip.cc/#manage', + 'Chibi', + 'popup=yes,status=no,location=no,toolbar=no,menubar=no' + ); } - export default openFrontend; diff --git a/src/global.d.ts b/src/global.d.ts index 1de4eab..bfea3c7 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -8,10 +8,13 @@ declare interface Window { vm?: VM; blockly?: ScratchBlocks | null; loader?: ChibiLoader; - registeredExtension: Record; - openFrontend (): void; - } + registeredExtension: Record< + string, + { + url: string; + env: string; + } + >; + openFrontend(): void; + }; } diff --git a/src/index.ts b/src/index.ts index caaa41c..ac7b295 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,14 @@ import { trap, inject } from './injector/inject'; import { log } from './util/log'; - +const open = window.open; // @ts-expect-error defined in webpack define plugin log(`Chibi ${__CHIBI_VERSION__}`); -await trap(); +// Try injecting chibi into the page. +await trap(open); if (typeof window.chibi.vm !== 'undefined') { + // Alright we got the virtual machine, start the injection. inject(window.chibi.vm); } else { + // This is not a Scratch page, stop injecting. log(`Cannot find vm in this page, stop injecting.`); } diff --git a/src/injector/inject.ts b/src/injector/inject.ts index 0bc8555..74b71e5 100644 --- a/src/injector/inject.ts +++ b/src/injector/inject.ts @@ -1,57 +1,79 @@ /// -import {log, warn, error} from '../util/log'; +import { log, warn, error } from '../util/log'; import { ChibiLoader } from '../loader/loader'; import openFrontend from '../frontend'; import type VM from 'scratch-vm'; import type Blockly from 'scratch-blocks'; +import setup from '../l10n/lang'; interface ChibiCompatibleWorkspace extends Blockly.Workspace { - registerButtonCallback (key: string, callback: Function): void; + registerButtonCallback(key: string, callback: Function): void; } interface ChibiCompatibleVM extends VM { ccExtensionManager?: { - info: Record; - getExtensionLoadOrder (extensions: string[]): unknown; + info: Record< + string, + { + api: number; + } + >; + getExtensionLoadOrder(extensions: string[]): unknown; }; setLocale?: (locale: string, ...args: unknown[]) => unknown; + getLocale?: () => string; } const MAX_LISTENING_MS = 30 * 1000; - -function getBlocklyInstance () { - const elem = document.querySelector('[class^="gui_blocks-wrapper"]'); - if (!elem) return null; - const internalKey = Object.keys(elem).find( - (key) => key.startsWith('__reactInternalInstance$') || - key.startsWith('__reactFiber$') - ); - if (!internalKey) return; - // @ts-expect-error - const internal = elem[internalKey]; - let childable = internal; - try { - while (((childable = childable.child), !childable || !childable.stateNode || !childable.stateNode.ScratchBlocks)) {} - } catch (e: unknown) { - return; +/** + * Get Blockly instance. + * @param vm Virtual machine instance. For some reasons we cannot use VM here. + * @returns Blockly instance. + */ +function getBlocklyInstance(vm: any): any | undefined { + // hijack Function.prototype.apply to get React element instance. + function hijack(fn: (...args: unknown[]) => void): any { + const _orig = Function.prototype.apply; + Function.prototype.apply = function (thisArg: any) { + return thisArg; + }; + const result = fn(); + Function.prototype.apply = _orig; + return result; } - return childable?.stateNode.ScratchBlocks; + const events = vm._events?.EXTENSION_ADDED; + if (events) { + if (events instanceof Function) { + // it is a function, just hijack it. + const result = hijack(events); + if (result && result.ScratchBlocks) return result.ScratchBlocks; + } else { + // it is an array, hijack every listeners. + for (const value of events) { + const result = hijack(value); + if (result && result.ScratchBlocks) return result.ScratchBlocks; + } + } + } + return undefined; // Method failed. } - -export function trap () { +/** + * Trap to get Virtual Machine instance. + * @param open window.open function (compatible with ccw). + * @return Callback promise. After that you could use window.chibi.vm to get the virtual machine. + */ +export function trap(open: typeof window.open): Promise { window.chibi = { // @ts-expect-error defined in webpack define plugin version: __CHIBI_VERSION__, registeredExtension: {}, - openFrontend: openFrontend + openFrontend: openFrontend.bind(null, open), }; log('Listening bind function...'); const oldBind = Function.prototype.bind; - return new Promise(resolve => { + return new Promise((resolve) => { const timeoutId = setTimeout(() => { log('Cannot find vm instance, stop listening.'); Function.prototype.bind = oldBind; @@ -63,8 +85,11 @@ export function trap () { return oldBind.apply(this, args); } else if ( args[0] && - Object.prototype.hasOwnProperty.call(args[0], "editingTarget") && - Object.prototype.hasOwnProperty.call(args[0], "runtime") + Object.prototype.hasOwnProperty.call( + args[0], + 'editingTarget' + ) && + Object.prototype.hasOwnProperty.call(args[0], 'runtime') ) { log('VM detected!'); window.chibi.vm = args[0]; @@ -77,25 +102,57 @@ export function trap () { }; }); } - -export function inject (vm: ChibiCompatibleVM) { - const loader = window.chibi.loader = new ChibiLoader(vm); +/** + * Inject into the original virtual machine. + * @param vm {ChibiCompatibleVM} Original virtual machine instance. + */ +export function inject(vm: ChibiCompatibleVM) { + const loader = (window.chibi.loader = new ChibiLoader(vm)); const originalLoadFunc = vm.extensionManager.loadExtensionURL; - vm.extensionManager.loadExtensionURL = async function (extensionURL: string, ...args: unknown[]) { + const getLocale = vm.getLocale; + let lang = setup(getLocale ? getLocale.call(vm) : 'en'); + vm.extensionManager.loadExtensionURL = async function ( + extensionURL: string, + ...args: unknown[] + ) { if (extensionURL in window.chibi.registeredExtension) { const { url, env } = window.chibi.registeredExtension[extensionURL]; try { - if (confirm(`🤨 Project is trying to sideloading ${extensionURL} from ${url}${env ? ` in ${env} mode` : ''}. Do you want to load?`)) { - await loader.load(url, (env ? env : (confirm('🤨 Do you want to load it in the sandbox?') ? 'sandboxed' : 'unsandboxed')) as 'unsandboxed' | 'sandboxed'); + const res = env + ? confirm( + lang + .format('chibi.tryLoadInEnv') + .replace('{EXT_URL}', extensionURL) + .replace('{URL}', url) + .replace('{ENV}', env) + ) + : confirm( + lang + .format('chibi.tryLoad') + .replace('{EXT_URL}', extensionURL) + .replace('{URL}', url) + ); + if (res) { + await loader.load( + url, + (env + ? env + : confirm(lang.format('chibi.loadInSandbox')) + ? 'sandboxed' + : 'unsandboxed') as 'unsandboxed' | 'sandboxed' + ); const extensionId = loader.getIdByUrl(url); // @ts-expect-error internal hack - vm.extensionManager._loadedExtensions.set(extensionId, 'Chibi'); + vm.extensionManager._loadedExtensions.set( + extensionId, + 'Chibi' + ); } else { // @ts-expect-error internal hack return originalLoadFunc.call(this, extensionURL, ...args); } } catch (e: unknown) { - error('Error occurred while sideloading extension. To avoid interrupting the loading process, we chose to ignore this error.', e); + error(lang.format('chibi.errorIgnored'), e); } } else { // @ts-expect-error internal hack @@ -121,15 +178,20 @@ export function inject (vm: ChibiCompatibleVM) { obj.extensionEnvs = Object.assign({}, obj.extensionEnvs, envs); return JSON.stringify(obj); }; - + const originalDrserializeFunc = vm.deserializeProject; - vm.deserializeProject = function (projectJSON: Record, ...args: unknown[]) { + vm.deserializeProject = function ( + projectJSON: Record, + ...args: unknown[] + ) { if (typeof projectJSON.extensionURLs === 'object') { for (const id in projectJSON.extensionURLs) { window.chibi.registeredExtension[id] = { url: projectJSON.extensionURLs[id], - env: typeof projectJSON.extensionEnvs === 'object' ? - projectJSON.extensionEnvs[id] : 'sandboxed' + env: + typeof projectJSON.extensionEnvs === 'object' + ? projectJSON.extensionEnvs[id] + : 'sandboxed', }; } } @@ -139,35 +201,48 @@ export function inject (vm: ChibiCompatibleVM) { const originSetLocaleFunc = vm.setLocale; vm.setLocale = function (locale: string, ...args: unknown[]) { + lang = setup(locale); // @ts-expect-error internal hack const result = originSetLocaleFunc.call(this, locale, ...args); // @ts-expect-error lazy to extend VM interface vm.emit('LOCALE_CHANGED', locale); return result; }; - - const originalArgReporterBooleanFunc = vm.runtime._primitives['argument_reporter_boolean']; - vm.runtime._primitives['argument_reporter_boolean'] = function (args: Record, ...otherArgs: unknown[]) { + // TODO: compiler support + const originalArgReporterBooleanFunc = + vm.runtime._primitives['argument_reporter_boolean']; + vm.runtime._primitives['argument_reporter_boolean'] = function ( + args: Record, + ...otherArgs: unknown[] + ) { const chibiFlag = args.VALUE; switch (chibiFlag) { - case '🧐 Chibi Installed?': - return true; - default: - return originalArgReporterBooleanFunc.call(this, args, ...otherArgs); + case '🧐 Chibi?': + return true; + default: + return originalArgReporterBooleanFunc.call( + this, + args, + ...otherArgs + ); } - } + }; // Hack for ClipCC 3.2- versions if (typeof vm.ccExtensionManager === 'object') { - const originalGetOrderFunc = vm.ccExtensionManager.getExtensionLoadOrder; - vm.ccExtensionManager.getExtensionLoadOrder = function (extensions: string[], ...args: unknown[]) { + const originalGetOrderFunc = + vm.ccExtensionManager.getExtensionLoadOrder; + vm.ccExtensionManager.getExtensionLoadOrder = function ( + extensions: string[], + ...args: unknown[] + ) { for (const extensionId of extensions) { if ( !vm.ccExtensionManager!.info.hasOwnProperty(extensionId) && extensionId in window.chibi.registeredExtension ) { vm.ccExtensionManager!.info[extensionId] = { - api: 0 + api: 0, }; } } @@ -178,67 +253,7 @@ export function inject (vm: ChibiCompatibleVM) { // Blockly stuffs setTimeout(() => { - const blockly = window.chibi.blockly = getBlocklyInstance(); - if (!blockly) { - warn('Cannot find real blockly instance, try alternative method...'); - const originalProcedureCallback = window.Blockly?.getMainWorkspace().toolboxCategoryCallbacks_.PROCEDURE; - if (!originalProcedureCallback) { - error('alternative method failed, stop injecting'); - return; - } - window.Blockly.getMainWorkspace().toolboxCategoryCallbacks_.PROCEDURE = function ( - workspace: ChibiCompatibleWorkspace, - ...args: unknown[] - ) { - const xmlList = originalProcedureCallback.call(this, workspace, ...args); - // Add separator and label - const sep = document.createElement('sep'); - sep.setAttribute('gap', '36'); - xmlList.push(sep); - const label = document.createElement('label'); - label.setAttribute('text', '😎 Chibi'); - xmlList.push(label); - - // Add dashboard button - const dashboardButton = document.createElement('button'); - dashboardButton.setAttribute('text', 'Open Frontend'); - dashboardButton.setAttribute('callbackKey', 'CHIBI_FRONTEND'); - workspace.registerButtonCallback('CHIBI_FRONTEND', () => { - window.chibi.openFrontend(); - }); - xmlList.push(dashboardButton); - - // Add load from url button - const sideloadButton = document.createElement('button'); - sideloadButton.setAttribute('text', 'Sideload from URL'); - sideloadButton.setAttribute('callbackKey', 'CHIBI_SIDELOAD_FROM_URL'); - workspace.registerButtonCallback('CHIBI_SIDELOAD_FROM_URL', () => { - const url = prompt('Enter URL'); - if (!url) return; - const mode = confirm('Running in sandbox?') ? 'sandboxed' : 'unsandboxed'; - window.chibi.loader.load(url, mode); - }); - xmlList.push(sideloadButton); - - // Add chibi detection - const mutation = document.createElement('mutation'); - mutation.setAttribute('chibi', 'installed'); - const field = document.createElement('field'); - field.setAttribute('name', 'VALUE'); - field.innerHTML = '🧐 Chibi Installed?'; - const block = document.createElement('block'); - block.setAttribute('type', 'argument_reporter_boolean'); - block.setAttribute('gap', '16'); - block.appendChild(field); - block.appendChild(mutation); - xmlList.push(block); - return xmlList; - }; - const workspace = window.Blockly.getMainWorkspace(); - workspace.getToolbox().refreshSelection(); - workspace.toolboxRefreshEnabled_ = true; - return; - }; + const blockly = (window.chibi.blockly = getBlocklyInstance(vm)); const originalAddCreateButton_ = blockly.Procedures.addCreateButton_; blockly.Procedures.addCreateButton_ = function ( @@ -257,7 +272,10 @@ export function inject (vm: ChibiCompatibleVM) { // Add dashboard button const dashboardButton = document.createElement('button'); - dashboardButton.setAttribute('text', 'Open Frontend'); + dashboardButton.setAttribute( + 'text', + lang.format('chibi.openFrontend') + ); dashboardButton.setAttribute('callbackKey', 'CHIBI_FRONTEND'); workspace.registerButtonCallback('CHIBI_FRONTEND', () => { window.chibi.openFrontend(); @@ -266,12 +284,17 @@ export function inject (vm: ChibiCompatibleVM) { // Add load from url button const sideloadButton = document.createElement('button'); - sideloadButton.setAttribute('text', 'Sideload from URL'); - sideloadButton.setAttribute('callbackKey', 'CHIBI_SIDELOAD_FROM_URL'); + sideloadButton.setAttribute('text', lang.format('chibi.sideload')); + sideloadButton.setAttribute( + 'callbackKey', + 'CHIBI_SIDELOAD_FROM_URL' + ); workspace.registerButtonCallback('CHIBI_SIDELOAD_FROM_URL', () => { const url = prompt('Enter URL'); if (!url) return; - const mode = confirm('Running in sandbox?') ? 'sandboxed' : 'unsandboxed'; + const mode = confirm(lang.format('chibi.loadInSandbox')) + ? 'sandboxed' + : 'unsandboxed'; window.chibi.loader.load(url, mode); }); xmlList.push(sideloadButton); @@ -281,7 +304,7 @@ export function inject (vm: ChibiCompatibleVM) { mutation.setAttribute('chibi', 'installed'); const field = document.createElement('field'); field.setAttribute('name', 'VALUE'); - field.innerHTML = '🧐 Chibi Installed?'; + field.innerHTML = '🧐 Chibi?'; const block = document.createElement('block'); block.setAttribute('type', 'argument_reporter_boolean'); block.setAttribute('gap', '16'); diff --git a/src/l10n/lang.ts b/src/l10n/lang.ts new file mode 100644 index 0000000..5ea195e --- /dev/null +++ b/src/l10n/lang.ts @@ -0,0 +1,60 @@ +import { warn } from '../util/log'; +const l10n: Record> = { + 'zh-cn': { + 'chibi.openFrontend': '打开面板', + 'chibi.sideload': '从 URL 侧载插件', + 'chibi.errorIgnored': + '在加载扩展插件时出现错误。为了避免加载进程的中断,此错误已被忽略。', + 'chibi.tryLoad': '🤨 项目正从 {URL} 加载插件 {EXT_URL}。要加载么?', + 'chibi.tryLoadInEnv': + '🤨 项目正以 {ENV} 模式从 {URL} 加载插件 {EXT_URL}。要加载么?', + 'chibi.loadInSandbox': '🤨 要在沙箱模式中加载插件么?', + 'chibi.enterURL': '🌐 输入', + }, + en: { + 'chibi.openFrontend': 'Open Frontend', + 'chibi.sideload': 'Sideload from URL', + 'chibi.errorIgnored': + 'Error occurred while sideloading extension. To avoid interrupting the loading process, we chose to ignore this error.', + 'chibi.tryLoad': + '🤨 Project is trying to sideloading {EXT_URL} from {URL}. Do you want to load?', + 'chibi.tryLoadInEnv': + '🤨 Project is trying to sideloading {EXT_URL} from {URL} in {ENV} mode. Do you want to load?', + 'chibi.loadInSandbox': '🤨 Do you want to load it in the sandbox?', + 'chibi.enterURL': '🌐 Enter URL', + }, + ja: { + 'chibi.openFrontend': 'ダッシュボードを開く', + 'chibi.sideload': 'URL から拡張機能を導入', + 'chibi.errorIgnored': + '拡張機能のサイドロード中でエラーが発生しました。ロードの中断を防ぐために、このエラーは無視しました。', + 'chibi.tryLoad': + '🤨 プロジェクトは {URL} から {EXT_URL} をサイドロードしています。ロードしますか?', + 'chibi.tryLoadInEnv': + '🤨 プロジェクトは {ENV} モードで、{URL} から {EXT_URL} をサイドロードしています。ロードしますか?', + 'chibi.loadInSandbox': '🤨 サンドボックス環境でロードしますか?', + 'chibi.enterURL': '🌐 URL を入力してください。', + }, +}; +class Language { + lang: string; + constructor(lang: string) { + this.lang = lang; + } + format(id: string) { + if (l10n[this.lang] && id in l10n[this.lang]) { + return l10n[this.lang][id]; + } else if (l10n['en'] && id in l10n['en']) { + return l10n['en'][id]; + } + return id; + } +} +export default function setup(lang: string): Language { + if (!(lang in l10n)) { + warn( + '🥺 This language is currently not supported. Falling back to English.' + ); + } + return new Language(lang); +} diff --git a/src/loader/dispatch/central-dispatch.ts b/src/loader/dispatch/central-dispatch.ts index 261203e..40e5406 100644 --- a/src/loader/dispatch/central-dispatch.ts +++ b/src/loader/dispatch/central-dispatch.ts @@ -12,14 +12,15 @@ class _CentralDispatch extends SharedDispatch { * The constructor we will use to recognize workers. * @type {Worker | null} */ - workerClass: typeof Worker | null = (typeof Worker === 'undefined' ? null : Worker); + workerClass: typeof Worker | null = + typeof Worker === 'undefined' ? null : Worker; /** * List of workers attached to this dispatcher. * @type {Array} */ workers: Worker[] = []; _onMessage!: (worker: Worker, event: MessageEvent) => void; - constructor () { + constructor() { super(); /** * Map of channel name to worker or local service provider. @@ -38,11 +39,13 @@ class _CentralDispatch extends SharedDispatch { * @param {*} [args] - the arguments to be copied to the method, if any. * @returns {*} - the return value of the service method. */ - callSync (service: string, method: string, ...args: unknown[]) { - const {provider, isRemote} = this._getServiceProvider(service); + callSync(service: string, method: string, ...args: unknown[]) { + const { provider, isRemote } = this._getServiceProvider(service); if (provider) { if (isRemote) { - throw new Error(`Cannot use 'callSync' on remote provider for service ${service}.`); + throw new Error( + `Cannot use 'callSync' on remote provider for service ${service}.` + ); } return provider[method].apply(provider, args); } @@ -54,9 +57,11 @@ class _CentralDispatch extends SharedDispatch { * @param {string} service - a globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'. * @param {object} provider - a local object which provides this service. */ - setServiceSync (service: string, provider: any) { + setServiceSync(service: string, provider: any) { if (this.services.hasOwnProperty(service)) { - console.warn(`Central dispatch replacing existing service provider for ${service}`); + console.warn( + `Central dispatch replacing existing service provider for ${service}` + ); } this.services[service] = provider; } @@ -67,7 +72,7 @@ class _CentralDispatch extends SharedDispatch { * @param {object} provider - a local object which provides this service. * @returns {Promise} - a promise which will resolve once the service is registered. */ - setService (service: string, provider: any) { + setService(service: string, provider: any) { /** Return a promise for consistency with {@link WorkerDispatch#setService} */ try { this.setServiceSync(service, provider); @@ -81,15 +86,17 @@ class _CentralDispatch extends SharedDispatch { * The dispatcher will immediately attempt to "handshake" with the worker. * @param {Worker} worker - the worker to add into the dispatch system. */ - addWorker (worker: Worker) { + addWorker(worker: Worker) { if (this.workers.indexOf(worker) === -1) { this.workers.push(worker); worker.onmessage = this._onMessage.bind(this, worker); - this._remoteCall(worker, 'dispatch', 'handshake').catch(e => { + this._remoteCall(worker, 'dispatch', 'handshake').catch((e) => { console.error(`Could not handshake with worker: ${e}`); }); } else { - console.warn('Central dispatch ignoring attempt to add duplicate worker'); + console.warn( + 'Central dispatch ignoring attempt to add duplicate worker' + ); } } /** @@ -99,12 +106,18 @@ class _CentralDispatch extends SharedDispatch { * @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found * @protected */ - _getServiceProvider (service: string) { + _getServiceProvider(service: string) { const provider = this.services[service]; - return provider && { - provider, - isRemote: Boolean((this.workerClass && provider instanceof this.workerClass) || provider.isRemote) - }; + return ( + provider && { + provider, + isRemote: Boolean( + (this.workerClass && + provider instanceof this.workerClass) || + provider.isRemote + ), + } + ); } /** * Handle a call message sent to the dispatch service itself @@ -114,18 +127,20 @@ class _CentralDispatch extends SharedDispatch { * @returns {Promise|undefined} - a promise for the results of this operation, if appropriate * @protected */ - _onDispatchMessage (worker: Worker, message: DispatchCallMessage) { + _onDispatchMessage(worker: Worker, message: DispatchCallMessage) { let promise; switch (message.method) { - case 'setService': - if (!message.args) { - console.error('setService received empty argument'); + case 'setService': + if (!message.args) { + console.error('setService received empty argument'); + break; + } + promise = this.setService(String(message.args[0]), worker); break; - } - promise = this.setService(String(message.args[0]), worker); - break; - default: - console.error(`Central dispatch received message for unknown method: ${message.method}`); + default: + console.error( + `Central dispatch received message for unknown method: ${message.method}` + ); } return promise; } diff --git a/src/loader/dispatch/shared-dispatch.ts b/src/loader/dispatch/shared-dispatch.ts index e13d2fa..3ac76ca 100644 --- a/src/loader/dispatch/shared-dispatch.ts +++ b/src/loader/dispatch/shared-dispatch.ts @@ -73,7 +73,7 @@ class SharedDispatch { * @param {*} [args] - the arguments to be copied to the method, if any. * @returns {Promise} - a promise for the return value of the service method. */ - call (service: string, method: string, ...args: unknown[]) { + call(service: string, method: string, ...args: unknown[]) { return this.transferCall(service, method, null, ...args); } /** @@ -91,13 +91,24 @@ class SharedDispatch { * @param {*} [args] - the arguments to be copied to the method, if any. * @returns {Promise} - a promise for the return value of the service method. */ - transferCall (service: string, method: string, transfer: unknown, ...args: unknown[]) { + transferCall( + service: string, + method: string, + transfer: unknown, + ...args: unknown[] + ) { try { // @ts-expect-error TS(2339): It's not implemented. - const {provider, isRemote} = this._getServiceProvider(service); + const { provider, isRemote } = this._getServiceProvider(service); if (provider) { if (isRemote) { - return this._remoteTransferCall(provider, service, method, transfer, ...args); + return this._remoteTransferCall( + provider, + service, + method, + transfer, + ...args + ); } const result = provider[method](...args); return Promise.resolve(result); @@ -113,7 +124,7 @@ class SharedDispatch { * @returns {boolean} - true if the service is remote (calls must cross a Worker boundary), false otherwise. * @private */ - _isRemoteService (service: string): boolean { + _isRemoteService(service: string): boolean { // @ts-expect-error TS(2339): It's not implemented. return this._getServiceProvider(service).isRemote; } @@ -125,8 +136,19 @@ class SharedDispatch { * @param {*} [args] - the arguments to be copied to the method, if any. * @returns {Promise} - a promise for the return value of the service method. */ - _remoteCall (provider: any, service: string, method: string, ...args: unknown[]): Promise { - return this._remoteTransferCall(provider, service, method, null, ...args); + _remoteCall( + provider: any, + service: string, + method: string, + ...args: unknown[] + ): Promise { + return this._remoteTransferCall( + provider, + service, + method, + null, + ...args + ); } /** * Like {@link transferCall}, but force the call to be posted through a particular communication channel. @@ -137,16 +159,25 @@ class SharedDispatch { * @param {*} [args] - the arguments to be copied to the method, if any. * @returns {Promise} - a promise for the return value of the service method. */ - _remoteTransferCall (provider: Worker, service: string, method: string, transfer: unknown, ...args: unknown[]) { + _remoteTransferCall( + provider: Worker, + service: string, + method: string, + transfer: unknown, + ...args: unknown[] + ) { return new Promise((resolve, reject) => { const responseId = this._storeCallbacks(resolve, reject); if (args) { args = this._purifyObject(args) as unknown[]; } if (transfer) { - provider.postMessage({service, method, responseId, args}, transfer); + provider.postMessage( + { service, method, responseId, args }, + transfer + ); } else { - provider.postMessage({service, method, responseId, args}); + provider.postMessage({ service, method, responseId, args }); } }); } @@ -157,7 +188,10 @@ class SharedDispatch { * @returns {*} - a unique response ID for this set of callbacks. See {@link _deliverResponse}. * @protected */ - _storeCallbacks (resolve: (value: unknown) => void, reject: (value: unknown) => void) { + _storeCallbacks( + resolve: (value: unknown) => void, + reject: (value: unknown) => void + ) { const responseId = this.nextResponseId++; this.callbacks[responseId] = [resolve, reject]; return responseId; @@ -168,7 +202,7 @@ class SharedDispatch { * @param {DispatchResponseMessage} message - the message containing the response value(s). * @protected */ - _deliverResponse (responseId: number, message: DispatchResponseMessage) { + _deliverResponse(responseId: number, message: DispatchResponseMessage) { try { const [resolve, reject] = this.callbacks[responseId]; delete this.callbacks[responseId]; @@ -187,7 +221,7 @@ class SharedDispatch { * @param {MessageEvent} event - the message event to be handled. * @protected */ - _onMessage (worker: Worker, event: MessageEvent) { + _onMessage(worker: Worker, event: MessageEvent) { /** @type {DispatchMessage} */ const message = event.data; message.args = message.args || []; @@ -196,18 +230,41 @@ class SharedDispatch { if (message.service === 'dispatch') { promise = this._onDispatchMessage(worker, message); } else { - promise = this.call(message.service, message.method, ...message.args); + promise = this.call( + message.service, + message.method, + ...message.args + ); } } else if (typeof message.responseId === 'undefined') { - console.error(`Dispatch caught malformed message from a worker: ${JSON.stringify(event)}`); + console.error( + `Dispatch caught malformed message from a worker: ${JSON.stringify( + event + )}` + ); } else { this._deliverResponse(message.responseId, message); } if (promise) { if (typeof message.responseId === 'undefined') { - console.error(`Dispatch message missing required response ID: ${JSON.stringify(event)}`); + console.error( + `Dispatch message missing required response ID: ${JSON.stringify( + event + )}` + ); } else { - promise.then(result => worker.postMessage({responseId: message.responseId, result}), error => worker.postMessage({responseId: message.responseId, error: `${error}`})); + promise.then( + (result) => + worker.postMessage({ + responseId: message.responseId, + result, + }), + (error) => + worker.postMessage({ + responseId: message.responseId, + error: `${error}`, + }) + ); } } } @@ -218,8 +275,10 @@ class SharedDispatch { * @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found * @protected */ - _getServiceProvider (service: string) { - throw new Error(`Could not get provider for ${service}: _getServiceProvider not implemented`); + _getServiceProvider(service: string) { + throw new Error( + `Could not get provider for ${service}: _getServiceProvider not implemented` + ); } /** * Handle a call message sent to the dispatch service itself @@ -229,8 +288,10 @@ class SharedDispatch { * @returns {Promise|undefined} - a promise for the results of this operation, if appropriate * @private */ - _onDispatchMessage (worker: Worker, message: DispatchCallMessage) { - throw new Error(`Unimplemented dispatch message handler cannot handle ${message.method} method`); + _onDispatchMessage(worker: Worker, message: DispatchCallMessage) { + throw new Error( + `Unimplemented dispatch message handler cannot handle ${message.method} method` + ); } /** @@ -238,18 +299,20 @@ class SharedDispatch { * @param {obj} object - The Object that need to be purified. * @returns {object} - purified object. */ - _purifyObject (obj: unknown, visited = new Set(), depth = 1): unknown { - if (typeof obj === "function" || typeof obj === "symbol") { + _purifyObject(obj: unknown, visited = new Set(), depth = 1): unknown { + if (typeof obj === 'function' || typeof obj === 'symbol') { return undefined; } - if (obj !== null && typeof obj === "object") { + if (obj !== null && typeof obj === 'object') { if (visited.has(obj)) return undefined; visited.add(obj); if (Array.isArray(obj)) { - return obj.map((item) => this._purifyObject(item, visited, depth + 1)); - } + return obj.map((item) => + this._purifyObject(item, visited, depth + 1) + ); + } const result: Record = {}; for (const key in obj) { // @ts-expect-error @@ -257,12 +320,9 @@ class SharedDispatch { result[key] = this._purifyObject(value, visited, depth + 1); } return result; - } return obj; } } -export { - SharedDispatch -}; +export { SharedDispatch }; diff --git a/src/loader/dispatch/worker-dispatch.ts b/src/loader/dispatch/worker-dispatch.ts index c4bc11a..6f85e19 100644 --- a/src/loader/dispatch/worker-dispatch.ts +++ b/src/loader/dispatch/worker-dispatch.ts @@ -1,7 +1,4 @@ -import { - DispatchCallMessage, - SharedDispatch -} from './shared-dispatch'; +import { DispatchCallMessage, SharedDispatch } from './shared-dispatch'; /** * This class provides a Worker with the means to participate in the message dispatch system managed by CentralDispatch. @@ -29,10 +26,10 @@ class _WorkerDispatch extends SharedDispatch { _onConnect!: (value?: unknown) => void; // @ts-expect-error _onMessage!: (worker: window & globalThis, event: MessageEvent) => void; - constructor () { + constructor() { super(); - this._connectionPromise = new Promise(resolve => { + this._connectionPromise = new Promise((resolve) => { this._onConnect = resolve; }); @@ -51,7 +48,7 @@ class _WorkerDispatch extends SharedDispatch { * dispatch.call('myService', 'hello'); * }) */ - get waitForConnection () { + get waitForConnection() { return this._connectionPromise; } @@ -62,12 +59,16 @@ class _WorkerDispatch extends SharedDispatch { * @param {object} provider - a local object which provides this service. * @returns {Promise} - a promise which will resolve once the service is registered. */ - setService (service: string, provider: unknown) { + setService(service: string, provider: unknown) { if (this.services.hasOwnProperty(service)) { - console.warn(`Worker dispatch replacing existing service provider for ${service}`); + console.warn( + `Worker dispatch replacing existing service provider for ${service}` + ); } this.services[service] = provider; - return this.waitForConnection.then(() => this._remoteCall(self, 'dispatch', 'setService', service)); + return this.waitForConnection.then(() => + this._remoteCall(self, 'dispatch', 'setService', service) + ); } /** @@ -77,12 +78,12 @@ class _WorkerDispatch extends SharedDispatch { * @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found * @protected */ - _getServiceProvider (service: string) { + _getServiceProvider(service: string) { // If we don't have a local service by this name, contact central dispatch by calling `postMessage` on self const provider = this.services[service]; return { provider: provider || self, - isRemote: !provider + isRemote: !provider, }; } @@ -94,19 +95,21 @@ class _WorkerDispatch extends SharedDispatch { * @returns {Promise|undefined} - a promise for the results of this operation, if appropriate * @protected */ - _onDispatchMessage (worker: Worker, message: DispatchCallMessage) { + _onDispatchMessage(worker: Worker, message: DispatchCallMessage) { let promise; switch (message.method) { - case 'handshake': - promise = this._onConnect(); - break; - case 'terminate': - // Don't close until next tick, after sending confirmation back - setTimeout(() => self.close(), 0); - promise = Promise.resolve(); - break; - default: - console.error(`Worker dispatch received message for unknown method: ${message.method}`); + case 'handshake': + promise = this._onConnect(); + break; + case 'terminate': + // Don't close until next tick, after sending confirmation back + setTimeout(() => self.close(), 0); + promise = Promise.resolve(); + break; + default: + console.error( + `Worker dispatch received message for unknown method: ${message.method}` + ); } return promise; } diff --git a/src/loader/loader.ts b/src/loader/loader.ts index 4660f04..31c2625 100644 --- a/src/loader/loader.ts +++ b/src/loader/loader.ts @@ -1,5 +1,5 @@ /// -import {log, warn, error} from '../util/log'; +import { log, warn, error } from '../util/log'; import { StandardScratchExtensionClass as ExtensionClass, ExtensionMetadata, @@ -7,18 +7,16 @@ import { ExtensionBlockMetadata, BlockType, MenuItems, - BlockArgs + BlockArgs, } from '../typings'; -import { - maybeFormatMessage -} from '../util/maybe-format-message'; +import { maybeFormatMessage } from '../util/maybe-format-message'; import { CentralDispatch as dispatch } from './dispatch/central-dispatch'; import { makeCtx } from './make-ctx'; import ExtensionSandbox from './sandbox.worker'; import type VM from 'scratch-vm'; interface PendingExtensionWorker { - extensionURL: string, + extensionURL: string; resolve: (value: unknown) => void; reject: (value: unknown) => void; } @@ -44,7 +42,7 @@ class ChibiLoader { * @type {int} */ private nextExtensionWorker = 0; - + /** * Whether Scratch object should be passed inline. * @type {boolean} @@ -71,16 +69,22 @@ class ChibiLoader { */ loadedScratchExtension = new Map(); - constructor (vm: VM) { + constructor(vm: VM) { this.vm = vm; this.inlinedCtx = typeof window.Scratch === 'object'; if (!this.inlinedCtx) { window.Scratch = makeCtx(this.vm); } else { - warn('A Scratch instance already exists in the current environment, so it will be passed inline for unsandboxed extension.'); + warn( + 'A Scratch instance already exists in the current environment, so it will be passed inline for unsandboxed extension.' + ); } dispatch.setService('loader', this).catch((e: Error) => { - error(`ChibiLoader was unable to register extension service: ${JSON.stringify(e)}`); + error( + `ChibiLoader was unable to register extension service: ${JSON.stringify( + e + )}` + ); }); } @@ -89,41 +93,54 @@ class ChibiLoader { * @param {ExtensionClass | string} ext - Extension's data. * @param {'sandboxed' | 'unsandboxed'} env - Extension's running environment. */ - async load (ext: string | ExtensionClass, env: 'sandboxed' | 'unsandboxed' = 'sandboxed') { + async load( + ext: string | ExtensionClass, + env: 'sandboxed' | 'unsandboxed' = 'sandboxed' + ) { if (typeof ext === 'string') { switch (env) { - case 'sandboxed': - return new Promise((resolve, reject) => { - // If we `require` this at the global level it breaks non-webpack targets, including tests - const ExtensionWorker = new ExtensionSandbox(); - this.pendingExtensions.push({ - extensionURL: ext, - resolve, - reject + case 'sandboxed': + return new Promise((resolve, reject) => { + // If we `require` this at the global level it breaks non-webpack targets, including tests + const ExtensionWorker = new ExtensionSandbox(); + this.pendingExtensions.push({ + extensionURL: ext, + resolve, + reject, + }); + dispatch.addWorker(ExtensionWorker); }); - dispatch.addWorker(ExtensionWorker); - }); - case 'unsandboxed': { - const response = await fetch(ext); - const originalScript = await response.text(); - const closureFunc = new Function('Scratch', originalScript); - const ctx = makeCtx(this.vm); - ctx.extensions.register = (extensionObj: ExtensionClass) => { - const extensionInfo = extensionObj.getInfo(); - this._registerExtensionInfo(extensionObj, extensionInfo, ext); - }; - closureFunc(ctx); - return; - } - default: - throw new Error('unexpected env'); + case 'unsandboxed': { + const response = await fetch(ext); + const originalScript = await response.text(); + const closureFunc = new Function('Scratch', originalScript); + const ctx = makeCtx(this.vm); + ctx.extensions.register = ( + extensionObj: ExtensionClass + ) => { + const extensionInfo = extensionObj.getInfo(); + this._registerExtensionInfo( + extensionObj, + extensionInfo, + ext + ); + }; + closureFunc(ctx); + return; + } + default: + throw new Error('unexpected env'); } } // @ts-expect-error Load as builtin extension. const extensionObject = new ext(this.vm.runtime); const extensionInfo = extensionObject.getInfo() as ExtensionMetadata; - this._registerExtensionInfo(extensionObject, extensionInfo, extensionInfo.id); + this._registerExtensionInfo( + extensionObject, + extensionInfo, + extensionInfo.id + ); return extensionInfo; } @@ -131,7 +148,7 @@ class ChibiLoader { * Reload a scratch-standard extension. * @param {string} extensionId - Extension's ID */ - async reload (extensionId: string) { + async reload(extensionId: string) { const targetExt = this.loadedScratchExtension.get(extensionId); if (!targetExt) { throw new Error(`Cannot locate extension ${extensionId}.`); @@ -139,7 +156,11 @@ class ChibiLoader { // It's running in worker if (typeof targetExt.instance === 'string') { const info = await dispatch.call(targetExt.instance, 'getInfo'); - const processedInfo = this._prepareExtensionInfo(null, info, targetExt.instance); + const processedInfo = this._prepareExtensionInfo( + null, + info, + targetExt.instance + ); // @ts-expect-error private method this.vm.runtime._refreshExtensionPrimitives(processedInfo); return processedInfo; @@ -154,7 +175,7 @@ class ChibiLoader { /** * Get all sideloaded extension infos. */ - getLoadedInfo () { + getLoadedInfo() { const extensionURLs: Record = {}; const extensionEnv: Record = {}; for (const [extId, ext] of this.loadedScratchExtension.entries()) { @@ -164,7 +185,7 @@ class ChibiLoader { return [extensionURLs, extensionEnv]; } - getIdByUrl (url: string) { + getIdByUrl(url: string) { for (const [extId, ext] of this.loadedScratchExtension.entries()) { if (ext.url === url) { return extId; @@ -178,7 +199,7 @@ class ChibiLoader { * original extension manager to reload locales. It should * be replaced when there's a better solution. */ - reloadAll () { + reloadAll() { const allPromises: Promise[] = []; for (const [extId] of this.loadedScratchExtension.entries()) { allPromises.push(this.reload(extId)); @@ -193,7 +214,12 @@ class ChibiLoader { * @param {string} serviceName - the name of the service hosting the extension * @private */ - private _registerExtensionInfo (extensionObject: ExtensionClass | null, extensionInfo: ExtensionMetadata, extensionURL: string, serviceName?: string) { + private _registerExtensionInfo( + extensionObject: ExtensionClass | null, + extensionInfo: ExtensionMetadata, + extensionURL: string, + serviceName?: string + ) { if (!this.loadedScratchExtension.has(extensionInfo.id)) { if (!extensionObject && !serviceName) { throw new Error(`Cannnot mark ${extensionInfo.id} as loaded.`); @@ -204,11 +230,17 @@ class ChibiLoader { id: extensionInfo.id, url: extensionURL, info: extensionInfo, - instance: (extensionObject ?? serviceName) as ExtensionClass | string, - env: serviceName ? 'sandboxed' : 'unsandboxed' + instance: (extensionObject ?? serviceName) as + | ExtensionClass + | string, + env: serviceName ? 'sandboxed' : 'unsandboxed', } as ScratchExtension); } - extensionInfo = this._prepareExtensionInfo(extensionObject, extensionInfo, serviceName); + extensionInfo = this._prepareExtensionInfo( + extensionObject, + extensionInfo, + serviceName + ); // @ts-expect-error private method this.vm.runtime._registerExtensionPrimitives(extensionInfo); @@ -220,7 +252,7 @@ class ChibiLoader { * @returns {string} - the sanitized text * @private */ - private _sanitizeID (text: string) { + private _sanitizeID(text: string) { return text.toString().replace(/[<"&]/, '_'); } @@ -233,7 +265,11 @@ class ChibiLoader { * @returns {ExtensionInfo} - a new extension info object with cleaned-up values * @private */ - private _prepareExtensionInfo (extensionObject: ExtensionClass | null, extensionInfo: ExtensionMetadata, serviceName?: string) { + private _prepareExtensionInfo( + extensionObject: ExtensionClass | null, + extensionInfo: ExtensionMetadata, + serviceName?: string + ) { extensionInfo = Object.assign({}, extensionInfo); if (!/^[a-z0-9]+$/i.test(extensionInfo.id)) { throw new Error('Invalid extension id'); @@ -241,26 +277,41 @@ class ChibiLoader { extensionInfo.name = extensionInfo.name || extensionInfo.id; extensionInfo.blocks = extensionInfo.blocks || []; extensionInfo.targetTypes = extensionInfo.targetTypes || []; - extensionInfo.blocks = extensionInfo.blocks.reduce((results: Array, blockInfo) => { - try { - let result; - switch (blockInfo) { - case '---': // Separator - result = '---'; - break; - default: // An ExtensionBlockMetadata object - result = this._prepareBlockInfo(extensionObject, blockInfo as ExtensionBlockMetadata, serviceName); - break; + extensionInfo.blocks = extensionInfo.blocks.reduce( + (results: Array, blockInfo) => { + try { + let result; + switch (blockInfo) { + case '---': // Separator + result = '---'; + break; + default: // An ExtensionBlockMetadata object + result = this._prepareBlockInfo( + extensionObject, + blockInfo as ExtensionBlockMetadata, + serviceName + ); + break; + } + results.push(result); + } catch (e: unknown) { + // TODO: more meaningful error reporting + error( + `Error processing block: ${ + (e as Error).message + }, Block:\n${JSON.stringify(blockInfo)}` + ); } - results.push(result); - } catch (e: unknown) { - // TODO: more meaningful error reporting - error(`Error processing block: ${(e as Error).message}, Block:\n${JSON.stringify(blockInfo)}`); - } - return results; - }, []); + return results; + }, + [] + ); extensionInfo.menus = extensionInfo.menus || {}; - extensionInfo.menus = this._prepareMenuInfo(extensionObject, extensionInfo.menus, serviceName); + extensionInfo.menus = this._prepareMenuInfo( + extensionObject, + extensionInfo.menus, + serviceName + ); return extensionInfo as ExtensionMetadata; } @@ -272,7 +323,11 @@ class ChibiLoader { * @returns {Array.} - a menuInfo object with all preprocessing done. * @private */ - private _prepareMenuInfo (extensionObject: ExtensionClass | null, menus: Record, serviceName?: string) { + private _prepareMenuInfo( + extensionObject: ExtensionClass | null, + menus: Record, + serviceName?: string + ) { const menuNames = Object.getOwnPropertyNames(menus); for (let i = 0; i < menuNames.length; i++) { const menuName = menuNames[i]; @@ -285,7 +340,7 @@ class ChibiLoader { if (!menuInfo.items) { menuInfo = { // @ts-expect-error - items: menuInfo + items: menuInfo, }; menus[menuName] = menuInfo; } @@ -296,7 +351,12 @@ class ChibiLoader { if (typeof menuInfo.items === 'string') { const menuItemFunctionName = menuInfo.items; // @ts-expect-error Bind the function here so we can pass a simple item generation function to Scratch Blocks later - menuInfo.items = this._getExtensionMenuItems.bind(this, extensionObject, menuItemFunctionName, serviceName); + menuInfo.items = this._getExtensionMenuItems.bind( + this, + extensionObject, + menuItemFunctionName, + serviceName + ); } } return menus; @@ -310,37 +370,52 @@ class ChibiLoader { * @returns {Array} menu items ready for scratch-blocks. * @private */ - private _getExtensionMenuItems (extensionObject: ExtensionClass, menuItemFunctionName: string, serviceName?: string): any[] { + private _getExtensionMenuItems( + extensionObject: ExtensionClass, + menuItemFunctionName: string, + serviceName?: string + ): any[] { /* * Fetch the items appropriate for the target currently being edited. This assumes that menus only * collect items when opened by the user while editing a particular target. */ - const editingTarget = this.vm.runtime.getEditingTarget() || this.vm.runtime.getTargetForStage(); + const editingTarget = + this.vm.runtime.getEditingTarget() || + this.vm.runtime.getTargetForStage(); const editingTargetID = editingTarget ? editingTarget.id : null; // @ts-expect-error private method - const extensionMessageContext = this.vm.runtime.makeMessageContextForTarget(editingTarget); + const extensionMessageContext = + this.vm.runtime.makeMessageContextForTarget(editingTarget); // TODO: Fix this to use dispatch.call when extensions are running in workers. - const menuFunc = extensionObject[menuItemFunctionName] as (editingTargetID: string | null) => MenuItems; - const menuItems = menuFunc.call(extensionObject, editingTargetID).map( - item => { + const menuFunc = extensionObject[menuItemFunctionName] as ( + editingTargetID: string | null + ) => MenuItems; + const menuItems = menuFunc + .call(extensionObject, editingTargetID) + .map((item) => { item = maybeFormatMessage(item, extensionMessageContext); switch (typeof item) { - case 'object': - return [ - maybeFormatMessage(item.text, extensionMessageContext), - item.value - ]; - case 'string': - return [item, item]; - default: - return item; + case 'object': + return [ + maybeFormatMessage( + item.text, + extensionMessageContext + ), + item.value, + ]; + case 'string': + return [item, item]; + default: + return item; } }); if (!menuItems || menuItems.length < 1) { - throw new Error(`Extension menu returned no items: ${menuItemFunctionName}`); + throw new Error( + `Extension menu returned no items: ${menuItemFunctionName}` + ); } return menuItems; } @@ -353,72 +428,109 @@ class ChibiLoader { * @returns {ExtensionBlockMetadata} - a new block info object which has values for all relevant optional fields. * @private */ - private _prepareBlockInfo (extensionObject: ExtensionClass | null, blockInfo: ExtensionBlockMetadata, serviceName?: string) { - blockInfo = Object.assign({}, { - blockType: BlockType.COMMAND, - terminal: false, - blockAllThreads: false, - arguments: {} - }, blockInfo); - blockInfo.opcode = blockInfo.opcode && this._sanitizeID(blockInfo.opcode); + private _prepareBlockInfo( + extensionObject: ExtensionClass | null, + blockInfo: ExtensionBlockMetadata, + serviceName?: string + ) { + blockInfo = Object.assign( + {}, + { + blockType: BlockType.COMMAND, + terminal: false, + blockAllThreads: false, + arguments: {}, + }, + blockInfo + ); + blockInfo.opcode = + blockInfo.opcode && this._sanitizeID(blockInfo.opcode); blockInfo.text = blockInfo.text || blockInfo.opcode; switch (blockInfo.blockType) { - case BlockType.EVENT: - if (blockInfo.func) { - warn(`Ignoring function "${blockInfo.func}" for event block ${blockInfo.opcode}`); - } - break; - case BlockType.BUTTON: - if (blockInfo.opcode) { - warn(`Ignoring opcode "${blockInfo.opcode}" for button with text: ${blockInfo.text}`); - } - break; - default: { - if (!blockInfo.opcode) { - throw new Error('Missing opcode for block'); - } - - const funcName = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode; - - const getBlockInfo = blockInfo.isDynamic ? - (args: BlockArgs) => args && args.mutation && args.mutation.blockInfo : - () => blockInfo; - const callBlockFunc = (() => { - // Maybe there's a worker - if (extensionObject === null) { - if (serviceName && dispatch._isRemoteService(serviceName)) { - return (args: BlockArgs, _util: unknown, realBlockInfo: unknown) => - dispatch.call(serviceName, funcName, args, undefined, realBlockInfo); - } - warn(`Could not find extension block function called ${funcName}`); - // eslint-disable-next-line @typescript-eslint/no-empty-function - return () => {}; + case BlockType.EVENT: + if (blockInfo.func) { + warn( + `Ignoring function "${blockInfo.func}" for event block ${blockInfo.opcode}` + ); } - - if (!extensionObject[funcName]) { - // The function might show up later as a dynamic property of the service object - warn(`Could not find extension block function called ${funcName}`); + break; + case BlockType.BUTTON: + if (blockInfo.opcode) { + warn( + `Ignoring opcode "${blockInfo.opcode}" for button with text: ${blockInfo.text}` + ); } - return (args: BlockArgs, util: unknown, realBlockInfo: unknown) => - // @ts-expect-error - extensionObject[funcName](args, util, realBlockInfo); - })(); - - // @ts-expect-error - blockInfo.func = (args: BlockArgs, util: unknown) => { - const realBlockInfo = getBlockInfo(args); - // TODO: filter args using the keys of realBlockInfo.arguments? maybe only if sandboxed? - return callBlockFunc(args, util, realBlockInfo); - }; - break; - } + break; + default: { + if (!blockInfo.opcode) { + throw new Error('Missing opcode for block'); + } + + const funcName = blockInfo.func + ? this._sanitizeID(blockInfo.func) + : blockInfo.opcode; + + const getBlockInfo = blockInfo.isDynamic + ? (args: BlockArgs) => + args && args.mutation && args.mutation.blockInfo + : () => blockInfo; + const callBlockFunc = (() => { + // Maybe there's a worker + if (extensionObject === null) { + if ( + serviceName && + dispatch._isRemoteService(serviceName) + ) { + return ( + args: BlockArgs, + _util: unknown, + realBlockInfo: unknown + ) => + dispatch.call( + serviceName, + funcName, + args, + undefined, + realBlockInfo + ); + } + warn( + `Could not find extension block function called ${funcName}` + ); + // eslint-disable-next-line @typescript-eslint/no-empty-function + return () => {}; + } + + if (!extensionObject[funcName]) { + // The function might show up later as a dynamic property of the service object + warn( + `Could not find extension block function called ${funcName}` + ); + } + return ( + args: BlockArgs, + util: unknown, + realBlockInfo: unknown + ) => + // @ts-expect-error + extensionObject[funcName](args, util, realBlockInfo); + })(); + + // @ts-expect-error + blockInfo.func = (args: BlockArgs, util: unknown) => { + const realBlockInfo = getBlockInfo(args); + // TODO: filter args using the keys of realBlockInfo.arguments? maybe only if sandboxed? + return callBlockFunc(args, util, realBlockInfo); + }; + break; + } } return blockInfo; } - async updateLocales () { + async updateLocales() { await this.reloadAll(); } @@ -426,11 +538,11 @@ class ChibiLoader { * Regenerate blockinfo for any loaded extensions * @returns {Promise} resolved once all the extensions have been reinitialized */ - async refreshBlocks () { + async refreshBlocks() { await this.reloadAll(); } - allocateWorker () { + allocateWorker() { const workerInfo = this.pendingExtensions.shift(); if (!workerInfo) { warn('pending extension queue is empty'); @@ -445,7 +557,7 @@ class ChibiLoader { * Collect extension metadata from the specified service and begin the extension registration process. * @param {string} serviceName - the name of the service hosting the extension. */ - async registerExtensionService (extensionURL: string, serviceName: string) { + async registerExtensionService(extensionURL: string, serviceName: string) { const info = await dispatch.call(serviceName, 'getInfo'); this._registerExtensionInfo(null, info, extensionURL, serviceName); } @@ -455,7 +567,7 @@ class ChibiLoader { * @param {int} id - the worker ID. * @param {*?} e - the error encountered during initialization, if any. */ - onWorkerInit (id: number, e?: Error) { + onWorkerInit(id: number, e?: Error) { const workerInfo = this.pendingWorkers[id]; delete this.pendingWorkers[id]; if (e) { @@ -466,6 +578,4 @@ class ChibiLoader { } } -export { - ChibiLoader -}; +export { ChibiLoader }; diff --git a/src/loader/make-ctx.ts b/src/loader/make-ctx.ts index 8d20776..906d1ae 100644 --- a/src/loader/make-ctx.ts +++ b/src/loader/make-ctx.ts @@ -3,14 +3,14 @@ import { TargetType, ArgumentType, ReporterScope, - StandardScratchExtensionClass as ExtensionClass + StandardScratchExtensionClass as ExtensionClass, } from '../typings'; import { Cast } from '../util/cast'; import formatMessage, { Message } from 'format-message'; import type VM from 'scratch-vm'; import type Renderer from 'scratch-render'; -export interface Ctx { +export interface Context { ArgumentType: typeof ArgumentType; BlockType: typeof BlockType; TargetType: typeof TargetType; @@ -18,15 +18,19 @@ export interface Ctx { Cast: Cast; translate: ReturnType; extensions: { - register: (extensionObj: ExtensionClass) => void, - unsandboxed: boolean, - chibi: true - } + register: (extensionObj: ExtensionClass) => void; + unsandboxed: boolean; + chibi: true; + }; vm?: VM; renderer?: Renderer; } - -function createTranslate (vm?: VM) { +/** + * i10n support for Chibi extensions. + * @param vm Virtual machine instance. Optional. + * @returns Something like Scratch.translate. + */ +function createTranslate(vm?: VM) { const namespace = formatMessage.namespace(); const translate = (message: Message, args?: object) => { @@ -34,7 +38,7 @@ function createTranslate (vm?: VM) { // Already in the expected format } else if (typeof message === 'string') { message = { - default: message + default: message, }; } else { throw new Error('unsupported data type in translate()'); @@ -47,7 +51,7 @@ function createTranslate (vm?: VM) { const getLocale = () => { // @ts-expect-error lazy to extend VM interface if (vm) return vm.getLocale(); - if (typeof navigator !== 'undefined') return navigator.language; + if (typeof navigator !== 'undefined') return navigator.language; // FIXME: en-US -> en return 'en'; }; @@ -60,7 +64,7 @@ function createTranslate (vm?: VM) { locale: getLocale(), missingTranslation: 'ignore', generateId, - translations: storedTranslations + translations: storedTranslations, }); }; @@ -75,9 +79,13 @@ function createTranslate (vm?: VM) { return translate; } - -export function makeCtx (vm?: VM) { - const ctx: Ctx = { +/** + * Make a fake scratch context. + * @param vm Virtual machine instance. + * @returns The context. + */ +export function makeCtx(vm?: VM) { + const ctx: Context = { ArgumentType: ArgumentType, BlockType: BlockType, TargetType: TargetType, @@ -88,9 +96,9 @@ export function makeCtx (vm?: VM) { throw new Error('not implemented'); }, unsandboxed: !!vm, - chibi: true + chibi: true, }, - translate: createTranslate(vm) + translate: createTranslate(vm), }; if (vm) { ctx.vm = vm; diff --git a/src/loader/sandbox.worker.ts b/src/loader/sandbox.worker.ts index 80d63ce..6330f3d 100644 --- a/src/loader/sandbox.worker.ts +++ b/src/loader/sandbox.worker.ts @@ -1,21 +1,23 @@ /* eslint-env worker */ -import { makeCtx, Ctx } from './make-ctx'; +import { makeCtx, Context } from './make-ctx'; import { WorkerDispatch as dispatch } from './dispatch/worker-dispatch'; declare global { - // eslint-disable-next-line no-var - var Scratch: Ctx; + // eslint-disable-next-line no-var + var Scratch: Context; } - +/** + * Here we implements a Worker dispatcher for sandboxed extensions. + */ class ExtensionWorker { nextExtensionId = 0; initialRegistrations: Promise[] = []; extensions: unknown[] = []; workerId?: number; extensionURL = ''; - constructor () { + constructor() { dispatch.waitForConnection.then(() => { - dispatch.call('loader', 'allocateWorker').then(x => { + dispatch.call('loader', 'allocateWorker').then((x) => { const [id, url] = x; this.workerId = id; this.extensionURL = url; @@ -26,7 +28,9 @@ class ExtensionWorker { const initialRegistrations = this.initialRegistrations; this.initialRegistrations = []; - Promise.all(initialRegistrations).then(() => dispatch.call('scratchAdapter', 'onWorkerInit', id)); + Promise.all(initialRegistrations).then(() => + dispatch.call('scratchAdapter', 'onWorkerInit', id) + ); } catch (e) { dispatch.call('scratchAdapter', 'onWorkerInit', id, e); } @@ -36,12 +40,20 @@ class ExtensionWorker { this.extensions = []; } - register (extensionObject: unknown) { + register(extensionObject: unknown) { const extensionId = this.nextExtensionId++; this.extensions.push(extensionObject); const serviceName = `extension.${this.workerId}.${extensionId}`; - const promise = dispatch.setService(serviceName, extensionObject) - .then(() => dispatch.call('loader', 'registerExtensionService', this.extensionURL, serviceName)); + const promise = dispatch + .setService(serviceName, extensionObject) + .then(() => + dispatch.call( + 'loader', + 'registerExtensionService', + this.extensionURL, + serviceName + ) + ); if (this.initialRegistrations) { this.initialRegistrations.push(promise); } @@ -55,6 +67,7 @@ globalThis.Scratch = makeCtx(); * Expose only specific parts of the worker to extensions. */ const extensionWorker = new ExtensionWorker(); -globalThis.Scratch.extensions.register = extensionWorker.register.bind(extensionWorker); +globalThis.Scratch.extensions.register = + extensionWorker.register.bind(extensionWorker); export default null as unknown as any; diff --git a/src/typings/argument-type.ts b/src/typings/argument-type.ts index 41c73e5..9b79979 100644 --- a/src/typings/argument-type.ts +++ b/src/typings/argument-type.ts @@ -34,9 +34,7 @@ enum ArgumentType { /** * Inline image on block (as part of the label) */ - IMAGE = 'image' + IMAGE = 'image', } -export { - ArgumentType -}; +export { ArgumentType }; diff --git a/src/typings/block-type.ts b/src/typings/block-type.ts index 12ecc1e..a810959 100644 --- a/src/typings/block-type.ts +++ b/src/typings/block-type.ts @@ -41,9 +41,7 @@ enum BlockType { /** * Lambda reporter with callable function. */ - Lambda = 'Lambda' + Lambda = 'Lambda', } -export { - BlockType -}; +export { BlockType }; diff --git a/src/typings/extension-metadata.ts b/src/typings/extension-metadata.ts index 7d0feb3..4b74526 100644 --- a/src/typings/extension-metadata.ts +++ b/src/typings/extension-metadata.ts @@ -8,15 +8,19 @@ export type MenuItemFunction = () => { value: string; }; -export type MenuItems = Array; +export type MenuItems = Array< + | string + | MenuItemFunction + | { + text: string; + value: string; + } +>; export interface BlockArgs { mutation?: { - blockInfo?: string - }, + blockInfo?: string; + }; [propName: string]: unknown; } @@ -39,7 +43,7 @@ export interface StandardScratchExtensionClass { getInfo: () => ExtensionMetadata; [propName: string]: unknown; } - + /** * All the metadata needed to register an extension. */ @@ -190,37 +194,37 @@ export interface ExtensionBlockMetadata { /** * List of target types for which this block should appear. */ - filter?: TargetType[] - } + filter?: TargetType[]; +} export interface ExtensionArgumentMetadata { /** * The type of the argument (number, string, etc.) */ - type: ArgumentType; - /** - * The default value of this argument - */ - defaultValue?: unknown; - /** - * The name of the menu to use for this argument, if any. - */ - menu?: string; - /** - * Only available when type is INLINE_IMAGE - */ - dataURI?: string; - /** - * Only available when type is INLINE_IMAGE - * Whether the image should be flipped horizontally when the editor has a right to left language selected as its locale. By default, the image is not flipped. - */ - flipRTL?: boolean; - /** - * Only available when type is INLINE_IMAGE - */ - alt?: string; - } - + type: ArgumentType; + /** + * The default value of this argument + */ + defaultValue?: unknown; + /** + * The name of the menu to use for this argument, if any. + */ + menu?: string; + /** + * Only available when type is INLINE_IMAGE + */ + dataURI?: string; + /** + * Only available when type is INLINE_IMAGE + * Whether the image should be flipped horizontally when the editor has a right to left language selected as its locale. By default, the image is not flipped. + */ + flipRTL?: boolean; + /** + * Only available when type is INLINE_IMAGE + */ + alt?: string; +} + /** * @typedef {ExtensionDynamicMenu|ExtensionMenuItems} ExtensionMenuMetadata * All the metadata needed to register an extension drop-down menu. diff --git a/src/typings/reporter-scope.ts b/src/typings/reporter-scope.ts index f5ec68e..2364721 100644 --- a/src/typings/reporter-scope.ts +++ b/src/typings/reporter-scope.ts @@ -12,9 +12,7 @@ enum ReporterScope { * This reporter's value is specific to a particular target/sprite. * Another target may have a different value or may not even have a value. */ - TARGET = 'target' + TARGET = 'target', } -export { - ReporterScope -}; +export { ReporterScope }; diff --git a/src/typings/target-type.ts b/src/typings/target-type.ts index bdc3c30..0009163 100644 --- a/src/typings/target-type.ts +++ b/src/typings/target-type.ts @@ -10,8 +10,6 @@ enum TargetType { /** * Rendered target which cannot move but can change backdrops */ - STAGE = 'stage' + STAGE = 'stage', } -export { - TargetType -}; +export { TargetType }; diff --git a/src/util/cast.ts b/src/util/cast.ts index a322a78..103d0d8 100644 --- a/src/util/cast.ts +++ b/src/util/cast.ts @@ -1,4 +1,4 @@ -import Color, {RGBObject} from '../util/color'; +import Color, { RGBObject } from '../util/color'; /** * @fileoverview @@ -44,7 +44,7 @@ class Cast { * @param {*} value Value to cast to number. * @return {number} The Scratch-casted number value. */ - static toNumber (value: unknown) : number { + static toNumber(value: unknown): number { /* * If value is already a number we don't need to coerce it with * Number(). @@ -77,16 +77,18 @@ class Cast { * @param {*} value Value to cast to boolean. * @return {boolean} The Scratch-casted boolean value. */ - static toBoolean (value: unknown) : boolean { + static toBoolean(value: unknown): boolean { // Already a boolean? if (typeof value === 'boolean') { return value; } if (typeof value === 'string') { // These specific strings are treated as false in Scratch. - if ((value === '') || - (value === '0') || - (value.toLowerCase() === 'false')) { + if ( + value === '' || + value === '0' || + value.toLowerCase() === 'false' + ) { return false; } // All other strings treated as true. @@ -101,7 +103,7 @@ class Cast { * @param {*} value Value to cast to string. * @return {string} The Scratch-casted string value. */ - static toString (value: unknown) { + static toString(value: unknown) { return String(value); } @@ -110,7 +112,7 @@ class Cast { * @param {*} value Value to convert to RGB color array. * @return {Array.} [r,g,b], values between 0-255. */ - static toRgbColorList (value: unknown) : [number, number, number] { + static toRgbColorList(value: unknown): [number, number, number] { const color = Cast.toRgbColorObject(value); return [color.r, color.g, color.b]; } @@ -120,13 +122,13 @@ class Cast { * @param {*} value Value to convert to RGB color object. * @return {RGBObject} [r,g,b], values between 0-255. */ - static toRgbColorObject (value: unknown) : RGBObject { + static toRgbColorObject(value: unknown): RGBObject { let color; if (typeof value === 'string' && value.substring(0, 1) === '#') { color = Color.hexToRgb(value); // If the color wasn't *actually* a hex color, cast to black - if (!color) color = {r: 0, g: 0, b: 0, a: 255}; + if (!color) color = { r: 0, g: 0, b: 0, a: 255 }; } else { color = Color.decimalToRgb(Cast.toNumber(value)); } @@ -138,8 +140,10 @@ class Cast { * @param {*} val value to check. * @return {boolean} True if the argument is all white spaces or null / empty. */ - static isWhiteSpace (val: unknown) { - return val === null || (typeof val === 'string' && val.trim().length === 0); + static isWhiteSpace(val: unknown) { + return ( + val === null || (typeof val === 'string' && val.trim().length === 0) + ); } /** @@ -149,7 +153,7 @@ class Cast { * @param {*} v2 Second value to compare. * @returns {number} Negative number if v1 < v2; 0 if equal; positive otherwise. */ - static compare (v1: unknown, v2: unknown) : number { + static compare(v1: unknown, v2: unknown): number { let n1 = Number(v1); let n2 = Number(v2); if (n1 === 0 && isNotActuallyZero(v1)) { @@ -187,10 +191,11 @@ class Cast { * @param {*} val Value to check. * @return {boolean} True if number looks like an integer. */ - static isInt (val: unknown) : boolean { + static isInt(val: unknown): boolean { // Values that are already numbers. if (typeof val === 'number') { - if (isNaN(val)) { // NaN is considered an integer. + if (isNaN(val)) { + // NaN is considered an integer. return true; } // True if it's "round" (e.g., 2.0 and 2). @@ -205,11 +210,11 @@ class Cast { return false; } - static get LIST_INVALID () { + static get LIST_INVALID() { return 'INVALID'; } - static get LIST_ALL () { + static get LIST_ALL() { return 'ALL'; } @@ -223,7 +228,7 @@ class Cast { * @param {boolean} acceptAll Whether it should accept "all" or not. * @return {(number|string)} 1-based index for list, LIST_ALL, or LIST_INVALID. */ - static toListIndex (index : ListIndex, length: number, acceptAll: boolean) { + static toListIndex(index: ListIndex, length: number, acceptAll: boolean) { if (typeof index !== 'number') { if (index === 'all') { return acceptAll ? Cast.LIST_ALL : Cast.LIST_INVALID; @@ -248,6 +253,4 @@ class Cast { } } -export { - Cast -}; +export { Cast }; diff --git a/src/util/color.ts b/src/util/color.ts index 879498b..b8249fb 100644 --- a/src/util/color.ts +++ b/src/util/color.ts @@ -26,13 +26,13 @@ class Color { */ /** @type {RGBObject} */ - static get RGB_BLACK () { - return {r: 0, g: 0, b: 0}; + static get RGB_BLACK() { + return { r: 0, g: 0, b: 0 }; } /** @type {RGBObject} */ - static get RGB_WHITE () { - return {r: 255, g: 255, b: 255}; + static get RGB_WHITE() { + return { r: 255, g: 255, b: 255 }; } /** @@ -40,9 +40,9 @@ class Color { * @param {number} decimal RGB color as a decimal. * @return {string} RGB color as #RRGGBB hex string. */ - static decimalToHex (decimal: number) { + static decimalToHex(decimal: number) { if (decimal < 0) { - decimal += 0xFFFFFF + 1; + decimal += 0xffffff + 1; } let hex = Number(decimal).toString(16); hex = `#${'000000'.substr(0, 6 - hex.length)}${hex}`; @@ -54,12 +54,12 @@ class Color { * @param {number} decimal RGB color as decimal. * @return {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}. */ - static decimalToRgb (decimal: number) { - const a = (decimal >> 24) & 0xFF; - const r = (decimal >> 16) & 0xFF; - const g = (decimal >> 8) & 0xFF; - const b = decimal & 0xFF; - return {r: r, g: g, b: b, a: a > 0 ? a : 255}; + static decimalToRgb(decimal: number) { + const a = (decimal >> 24) & 0xff; + const r = (decimal >> 16) & 0xff; + const g = (decimal >> 8) & 0xff; + const b = decimal & 0xff; + return { r: r, g: g, b: b, a: a > 0 ? a : 255 }; } /** @@ -67,7 +67,7 @@ class Color { * @param {!string} hex Hex representation of the color. * @return {RGBObject | null} null on failure, or rgb: {r: red [0,255], g: green [0,255], b: blue [0,255]}. */ - static hexToRgb (hex: string) : RGBObject | null { + static hexToRgb(hex: string): RGBObject | null { if (hex.startsWith('#')) { hex = hex.substring(1); } @@ -79,16 +79,16 @@ class Color { return { r: (parsed >> 16) & 0xff, g: (parsed >> 8) & 0xff, - b: parsed & 0xff + b: parsed & 0xff, }; } else if (hex.length === 3) { - const r = ((parsed >> 8) & 0xf); - const g = ((parsed >> 4) & 0xf); + const r = (parsed >> 8) & 0xf; + const g = (parsed >> 4) & 0xf; const b = parsed & 0xf; return { r: (r << 4) | r, g: (g << 4) | g, - b: (b << 4) | b + b: (b << 4) | b, }; } return null; @@ -99,7 +99,7 @@ class Color { * @param {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}. * @return {!string} Hex representation of the color. */ - static rgbToHex (rgb: RGBObject) { + static rgbToHex(rgb: RGBObject) { return Color.decimalToHex(Color.rgbToDecimal(rgb)); } @@ -108,7 +108,7 @@ class Color { * @param {RGBObject | null} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}. * @return {!number} Number representing the color. */ - static rgbToDecimal (rgb: RGBObject | null) { + static rgbToDecimal(rgb: RGBObject | null) { if (rgb === null) throw new Error('rgb must be an RGBObject'); return (rgb.r << 16) + (rgb.g << 8) + rgb.b; } @@ -118,7 +118,7 @@ class Color { * @param {!string} hex Hex representation of the color. * @return {!number} Number representing the color. */ - static hexToDecimal (hex: string) { + static hexToDecimal(hex: string) { return Color.rgbToDecimal(Color.hexToRgb(hex)); } @@ -127,60 +127,60 @@ class Color { * @param {HSVObject} hsv - {h: hue [0,360), s: saturation [0,1], v: value [0,1]} * @return {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}. */ - static hsvToRgb (hsv: HSVObject) { + static hsvToRgb(hsv: HSVObject) { let h = hsv.h % 360; if (h < 0) h += 360; const s = Math.max(0, Math.min(hsv.s, 1)); const v = Math.max(0, Math.min(hsv.v, 1)); const i = Math.floor(h / 60); - const f = (h / 60) - i; + const f = h / 60 - i; const p = v * (1 - s); - const q = v * (1 - (s * f)); - const t = v * (1 - (s * (1 - f))); + const q = v * (1 - s * f); + const t = v * (1 - s * (1 - f)); let r; let g; let b; switch (i) { - default: - case 0: - r = v; - g = t; - b = p; - break; - case 1: - r = q; - g = v; - b = p; - break; - case 2: - r = p; - g = v; - b = t; - break; - case 3: - r = p; - g = q; - b = v; - break; - case 4: - r = t; - g = p; - b = v; - break; - case 5: - r = v; - g = p; - b = q; - break; + default: + case 0: + r = v; + g = t; + b = p; + break; + case 1: + r = q; + g = v; + b = p; + break; + case 2: + r = p; + g = v; + b = t; + break; + case 3: + r = p; + g = q; + b = v; + break; + case 4: + r = t; + g = p; + b = v; + break; + case 5: + r = v; + g = p; + b = q; + break; } return { r: Math.floor(r * 255), g: Math.floor(g * 255), - b: Math.floor(b * 255) + b: Math.floor(b * 255), }; } @@ -189,7 +189,7 @@ class Color { * @param {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}. * @return {HSVObject} hsv - {h: hue [0,360), s: saturation [0,1], v: value [0,1]} */ - static rgbToHsv (rgb: RGBObject) { + static rgbToHsv(rgb: RGBObject) { const r = rgb.r / 255; const g = rgb.g / 255; const b = rgb.b / 255; @@ -200,13 +200,13 @@ class Color { let h = 0; let s = 0; if (x !== v) { - const f = (r === x) ? g - b : ((g === x) ? b - r : r - g); - const i = (r === x) ? 3 : ((g === x) ? 5 : 1); - h = ((i - (f / (v - x))) * 60) % 360; + const f = r === x ? g - b : g === x ? b - r : r - g; + const i = r === x ? 3 : g === x ? 5 : 1; + h = ((i - f / (v - x)) * 60) % 360; s = (v - x) / v; } - return {h: h, s: s, v: v}; + return { h: h, s: s, v: v }; } /** @@ -216,14 +216,14 @@ class Color { * @param {number} fraction1 - the interpolation parameter. If this is 0.5, for example, mix the two colors equally. * @return {RGBObject} the interpolated color. */ - static mixRgb (rgb0: RGBObject, rgb1: RGBObject, fraction1: number) { + static mixRgb(rgb0: RGBObject, rgb1: RGBObject, fraction1: number) { if (fraction1 <= 0) return rgb0; if (fraction1 >= 1) return rgb1; const fraction0 = 1 - fraction1; return { - r: (fraction0 * rgb0.r) + (fraction1 * rgb1.r), - g: (fraction0 * rgb0.g) + (fraction1 * rgb1.g), - b: (fraction0 * rgb0.b) + (fraction1 * rgb1.b) + r: fraction0 * rgb0.r + fraction1 * rgb1.r, + g: fraction0 * rgb0.g + fraction1 * rgb1.g, + b: fraction0 * rgb0.b + fraction1 * rgb1.b, }; } } diff --git a/src/util/log.ts b/src/util/log.ts index 04a467f..99f4c79 100644 --- a/src/util/log.ts +++ b/src/util/log.ts @@ -1,11 +1,33 @@ -export function log (...params: unknown[]) { - console.log('%c😎 Chibi', ` background-color: #f7c7bb; border-radius: 1rem; margin-right: 0.25rem; padding: 0 0.5rem; color: #271919;`, ...params); +/** + * Output general messages. + * @param params The message. + */ +export function log(...params: unknown[]) { + console.log( + '%c😎 Chibi', + ` background-color: #f7c7bb; border-radius: 1rem; margin-right: 0.25rem; padding: 0 0.5rem; color: #271919;`, + ...params + ); } - -export function warn (...params: unknown[]) { - console.warn('%c😨 Chibi', ` background-color: #f7c7bb; border-radius: 1rem; margin-right: 0.25rem; padding: 0 0.5rem; color: #271919;`, ...params); +/** + * Output warning messages. + * @param params The message. + */ +export function warn(...params: unknown[]) { + console.warn( + '%c😨 Chibi', + ` background-color: #f7c7bb; border-radius: 1rem; margin-right: 0.25rem; padding: 0 0.5rem; color: #271919;`, + ...params + ); } - -export function error (...params: unknown[]) { - console.error('%c😵 Chibi', ` background-color: #f7c7bb; border-radius: 1rem; margin-right: 0.25rem; padding: 0 0.5rem; color: #271919;`, ...params); +/** + * Output error (exception) messages. + * @param params The message. + */ +export function error(...params: unknown[]) { + console.error( + '%c😵 Chibi', + ` background-color: #f7c7bb; border-radius: 1rem; margin-right: 0.25rem; padding: 0 0.5rem; color: #271919;`, + ...params + ); } diff --git a/src/util/maybe-format-message.ts b/src/util/maybe-format-message.ts index e060710..fbedb31 100644 --- a/src/util/maybe-format-message.ts +++ b/src/util/maybe-format-message.ts @@ -7,7 +7,11 @@ import formatMessage from 'format-message'; * @param {string} [locale] - the locale to pass to `formatMessage` if it gets called. * @return {string|*} - the formatted message OR the original `maybeMessage` input. */ -export const maybeFormatMessage = function (maybeMessage?: any, args?: any, locale?: any) { +export const maybeFormatMessage = function ( + maybeMessage?: any, + args?: any, + locale?: any +) { if (maybeMessage && maybeMessage.id && maybeMessage.default) { return formatMessage(maybeMessage, args, locale); } diff --git a/webpack.config.js b/webpack.config.js index be599b2..9e00f99 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -59,7 +59,7 @@ const prodConfig = { version: packageJSON.version, grant: ['none'], 'run-at': 'document-start', - include: [ + match: [ 'http://localhost:8601/*', 'https://scratch.mit.edu/projects/*', 'https://aerfaying.com/Projects/*', diff --git a/yarn.lock b/yarn.lock index 7b84378..b0be598 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3454,6 +3454,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" + integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== + pretty-error@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" From 182a699ed8ec1764106c7a50efb8e09f9e83c940 Mon Sep 17 00:00:00 2001 From: FurryR Date: Sun, 29 Oct 2023 19:50:38 +0800 Subject: [PATCH 2/3] :bug: Fixed typo --- src/l10n/lang.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/l10n/lang.ts b/src/l10n/lang.ts index 5ea195e..c3d9ea7 100644 --- a/src/l10n/lang.ts +++ b/src/l10n/lang.ts @@ -2,13 +2,13 @@ import { warn } from '../util/log'; const l10n: Record> = { 'zh-cn': { 'chibi.openFrontend': '打开面板', - 'chibi.sideload': '从 URL 侧载插件', + 'chibi.sideload': '从 URL 侧载扩展', 'chibi.errorIgnored': - '在加载扩展插件时出现错误。为了避免加载进程的中断,此错误已被忽略。', - 'chibi.tryLoad': '🤨 项目正从 {URL} 加载插件 {EXT_URL}。要加载么?', + '在加载扩展扩展时出现错误。为了避免加载进程的中断,此错误已被忽略。', + 'chibi.tryLoad': '🤨 项目正从 {URL} 加载扩展 {EXT_URL}。要加载么?', 'chibi.tryLoadInEnv': - '🤨 项目正以 {ENV} 模式从 {URL} 加载插件 {EXT_URL}。要加载么?', - 'chibi.loadInSandbox': '🤨 要在沙箱模式中加载插件么?', + '🤨 项目正以 {ENV} 模式从 {URL} 加载扩展 {EXT_URL}。要加载么?', + 'chibi.loadInSandbox': '🤨 要在沙箱模式中加载扩展么?', 'chibi.enterURL': '🌐 输入', }, en: { From 92afea0a484adbaad8968fe4b34a66eac8373bf4 Mon Sep 17 00:00:00 2001 From: FurryR Date: Sun, 29 Oct 2023 21:33:25 +0800 Subject: [PATCH 3/3] :bug: Fix several bugs --- .eslintrc.json | 3 +- .prettierrc.json | 6 +- README-ja_JP.md | 2 +- src/frontend/index.ts | 20 ++- src/injector/inject.ts | 159 ++++++++++++++++-------- src/l10n/l10n.json | 29 +++++ src/l10n/lang.ts | 60 --------- src/loader/dispatch/central-dispatch.ts | 21 +--- src/loader/dispatch/shared-dispatch.ts | 55 ++------ src/loader/dispatch/worker-dispatch.ts | 6 +- src/loader/loader.ts | 130 ++++++------------- src/loader/make-ctx.ts | 12 +- src/loader/sandbox.worker.ts | 10 +- src/typings/argument-type.ts | 2 +- src/typings/block-type.ts | 2 +- src/typings/reporter-scope.ts | 2 +- src/typings/target-type.ts | 2 +- src/util/cast.ts | 15 +-- src/util/color.ts | 8 +- src/util/maybe-format-message.ts | 6 +- tsconfig.json | 2 +- 21 files changed, 228 insertions(+), 324 deletions(-) create mode 100644 src/l10n/l10n.json delete mode 100644 src/l10n/lang.ts diff --git a/.eslintrc.json b/.eslintrc.json index 727b058..d9a14f0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,7 +14,7 @@ }, "rules": { "semi": "error", - "indent": ["error", 4], + "indent": ["error", 4, {"SwitchCase": 1}], "dot-notation": "error", "block-scoped-var": "error", "capitalized-comments": "warn", @@ -30,7 +30,6 @@ "func-call-spacing": "error", "dot-location": ["error", "property"], "no-whitespace-before-property": "error", - "space-before-function-paren": "error", "space-unary-ops": ["error", { "words": true, "nonwords": false diff --git a/.prettierrc.json b/.prettierrc.json index 2f3f1d9..5855764 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -2,9 +2,9 @@ "semi": true, "tabWidth": 4, "singleQuote": true, - "trailingComma": "es5", + "trailingComma": "none", "arrowParens": "always", "bracketSameLine": true, "bracketSpacing": true, - "printWidth": 80 -} \ No newline at end of file + "printWidth": 100 +} diff --git a/README-ja_JP.md b/README-ja_JP.md index a636a48..6b331de 100644 --- a/README-ja_JP.md +++ b/README-ja_JP.md @@ -26,7 +26,7 @@ Chibi は Tampermonkey/GreasyMonkey に対応するユーザースクリプト - [X] CodingClip - [X] Cocrea - [X] Aerfaying -- [X] Cocrea 中国版 +- [X] Co-Create World - [X] XMW - [X] CodeLab - [X] 40code diff --git a/src/frontend/index.ts b/src/frontend/index.ts index b3b3256..75349ca 100644 --- a/src/frontend/index.ts +++ b/src/frontend/index.ts @@ -25,13 +25,10 @@ type MothDispatched = MothDispatchedAllocate | MothDispatchedLoad; */ function getExtensionInfo() { const processedExtInfo: MothExtensionInfo[] = []; - for (const [ - extId, - ext, - ] of window.chibi.loader.loadedScratchExtension.entries()) { + for (const [extId, ext] of window.chibi.loader.loadedScratchExtension.entries()) { processedExtInfo.push({ name: extId, - sandboxed: ext.env === 'sandboxed', + sandboxed: ext.env === 'sandboxed' }); } return processedExtInfo; @@ -44,29 +41,29 @@ async function messageHandler(event: MessageEvent) { if (event.origin !== 'https://chibi.codingclip.cc') return; if (!('type' in event.data)) return; switch ((event.data as MothDispatched).type) { + // Handshake: send current extension info in order to prepare frontend. case 'allocate': - // Handshake: send current extension info in order to prepare frontend. console.log('handshake with frontend'); dashboardWindow?.postMessage( { type: 'handshake', clientInfo: { version: Number(window.chibi.version), - url: window.location.host, - }, + url: window.location.host + } }, '*' ); dashboardWindow?.postMessage( { type: 'extension', - extensions: getExtensionInfo(), + extensions: getExtensionInfo() }, '*' ); break; case 'load': - // Load a extension. + // Load an extension. await window.chibi.loader.load( event.data.info.url, event.data.info.sandboxed ? 'sandboxed' : 'unsandboxed' @@ -74,7 +71,7 @@ async function messageHandler(event: MessageEvent) { dashboardWindow?.postMessage( { type: 'extension', - extensions: getExtensionInfo(), + extensions: getExtensionInfo() }, '*' ); @@ -96,4 +93,5 @@ function openFrontend(open: typeof window.open) { 'popup=yes,status=no,location=no,toolbar=no,menubar=no' ); } + export default openFrontend; diff --git a/src/injector/inject.ts b/src/injector/inject.ts index 74b71e5..2302879 100644 --- a/src/injector/inject.ts +++ b/src/injector/inject.ts @@ -4,7 +4,8 @@ import { ChibiLoader } from '../loader/loader'; import openFrontend from '../frontend'; import type VM from 'scratch-vm'; import type Blockly from 'scratch-blocks'; -import setup from '../l10n/lang'; +import * as l10n from '../l10n/l10n.json'; +import formatMessage from 'format-message'; interface ChibiCompatibleWorkspace extends Blockly.Workspace { registerButtonCallback(key: string, callback: Function): void; @@ -58,6 +59,7 @@ function getBlocklyInstance(vm: any): any | undefined { } return undefined; // Method failed. } + /** * Trap to get Virtual Machine instance. * @param open window.open function (compatible with ccw). @@ -68,7 +70,7 @@ export function trap(open: typeof window.open): Promise { // @ts-expect-error defined in webpack define plugin version: __CHIBI_VERSION__, registeredExtension: {}, - openFrontend: openFrontend.bind(null, open), + openFrontend: openFrontend.bind(null, open) }; log('Listening bind function...'); @@ -85,10 +87,7 @@ export function trap(open: typeof window.open): Promise { return oldBind.apply(this, args); } else if ( args[0] && - Object.prototype.hasOwnProperty.call( - args[0], - 'editingTarget' - ) && + Object.prototype.hasOwnProperty.call(args[0], 'editingTarget') && Object.prototype.hasOwnProperty.call(args[0], 'runtime') ) { log('VM detected!'); @@ -102,6 +101,7 @@ export function trap(open: typeof window.open): Promise { }; }); } + /** * Inject into the original virtual machine. * @param vm {ChibiCompatibleVM} Original virtual machine instance. @@ -110,7 +110,13 @@ export function inject(vm: ChibiCompatibleVM) { const loader = (window.chibi.loader = new ChibiLoader(vm)); const originalLoadFunc = vm.extensionManager.loadExtensionURL; const getLocale = vm.getLocale; - let lang = setup(getLocale ? getLocale.call(vm) : 'en'); + let format = formatMessage.namespace(); + format.setup({ + locale: getLocale ? getLocale.call(vm) : 'en', + missingTranslation: 'ignore', + generateId: (defaultMessage: string) => `${defaultMessage}`, + translations: l10n + }); vm.extensionManager.loadExtensionURL = async function ( extensionURL: string, ...args: unknown[] @@ -120,39 +126,36 @@ export function inject(vm: ChibiCompatibleVM) { try { const res = env ? confirm( - lang - .format('chibi.tryLoadInEnv') - .replace('{EXT_URL}', extensionURL) - .replace('{URL}', url) - .replace('{ENV}', env) + format('chibi.tryLoadInEnv', { + extensionURL, + url, + env + }) ) : confirm( - lang - .format('chibi.tryLoad') - .replace('{EXT_URL}', extensionURL) - .replace('{URL}', url) + format('chibi.tryLoadInEnv', { + extensionURL, + url + }) ); if (res) { await loader.load( url, (env ? env - : confirm(lang.format('chibi.loadInSandbox')) + : confirm(format('chibi.loadInSandbox')) ? 'sandboxed' : 'unsandboxed') as 'unsandboxed' | 'sandboxed' ); const extensionId = loader.getIdByUrl(url); // @ts-expect-error internal hack - vm.extensionManager._loadedExtensions.set( - extensionId, - 'Chibi' - ); + vm.extensionManager._loadedExtensions.set(extensionId, 'Chibi'); } else { // @ts-expect-error internal hack return originalLoadFunc.call(this, extensionURL, ...args); } } catch (e: unknown) { - error(lang.format('chibi.errorIgnored'), e); + error(format('chibi.errorIgnored'), e); } } else { // @ts-expect-error internal hack @@ -180,10 +183,7 @@ export function inject(vm: ChibiCompatibleVM) { }; const originalDrserializeFunc = vm.deserializeProject; - vm.deserializeProject = function ( - projectJSON: Record, - ...args: unknown[] - ) { + vm.deserializeProject = function (projectJSON: Record, ...args: unknown[]) { if (typeof projectJSON.extensionURLs === 'object') { for (const id in projectJSON.extensionURLs) { window.chibi.registeredExtension[id] = { @@ -191,7 +191,7 @@ export function inject(vm: ChibiCompatibleVM) { env: typeof projectJSON.extensionEnvs === 'object' ? projectJSON.extensionEnvs[id] - : 'sandboxed', + : 'sandboxed' }; } } @@ -201,7 +201,12 @@ export function inject(vm: ChibiCompatibleVM) { const originSetLocaleFunc = vm.setLocale; vm.setLocale = function (locale: string, ...args: unknown[]) { - lang = setup(locale); + format.setup({ + locale, + missingTranslation: 'ignore', + generateId: (defaultMessage: string) => `${defaultMessage}`, + translations: l10n + }); // @ts-expect-error internal hack const result = originSetLocaleFunc.call(this, locale, ...args); // @ts-expect-error lazy to extend VM interface @@ -209,29 +214,26 @@ export function inject(vm: ChibiCompatibleVM) { return result; }; // TODO: compiler support - const originalArgReporterBooleanFunc = - vm.runtime._primitives['argument_reporter_boolean']; + const originalArgReporterBooleanFunc = vm.runtime._primitives['argument_reporter_boolean']; vm.runtime._primitives['argument_reporter_boolean'] = function ( args: Record, ...otherArgs: unknown[] ) { const chibiFlag = args.VALUE; switch (chibiFlag) { + case '🧐 Chibi Installed?': + warn("'🧐 Chibi Installed?' is deprecated, use '🧐 Chibi?' instead."); + return true; case '🧐 Chibi?': return true; default: - return originalArgReporterBooleanFunc.call( - this, - args, - ...otherArgs - ); + return originalArgReporterBooleanFunc.call(this, args, ...otherArgs); } }; // Hack for ClipCC 3.2- versions if (typeof vm.ccExtensionManager === 'object') { - const originalGetOrderFunc = - vm.ccExtensionManager.getExtensionLoadOrder; + const originalGetOrderFunc = vm.ccExtensionManager.getExtensionLoadOrder; vm.ccExtensionManager.getExtensionLoadOrder = function ( extensions: string[], ...args: unknown[] @@ -242,7 +244,7 @@ export function inject(vm: ChibiCompatibleVM) { extensionId in window.chibi.registeredExtension ) { vm.ccExtensionManager!.info[extensionId] = { - api: 0, + api: 0 }; } } @@ -254,7 +256,70 @@ export function inject(vm: ChibiCompatibleVM) { // Blockly stuffs setTimeout(() => { const blockly = (window.chibi.blockly = getBlocklyInstance(vm)); + // deprecated: this method will be removed in the future. + if (!blockly) { + warn('Cannot find real blockly instance, try alternative method...'); + const originalProcedureCallback = + window.Blockly?.getMainWorkspace().toolboxCategoryCallbacks_.PROCEDURE; + if (!originalProcedureCallback) { + error('alternative method failed, stop injecting'); + return; + } + window.Blockly.getMainWorkspace().toolboxCategoryCallbacks_.PROCEDURE = function ( + workspace: ChibiCompatibleWorkspace, + ...args: unknown[] + ) { + const xmlList = originalProcedureCallback.call(this, workspace, ...args); + // Add separator and label + const sep = document.createElement('sep'); + sep.setAttribute('gap', '36'); + xmlList.push(sep); + const label = document.createElement('label'); + label.setAttribute('text', '😎 Chibi'); + xmlList.push(label); + // Add dashboard button + const dashboardButton = document.createElement('button'); + dashboardButton.setAttribute('text', format('chibi.openFrontend')); + dashboardButton.setAttribute('callbackKey', 'CHIBI_FRONTEND'); + workspace.registerButtonCallback('CHIBI_FRONTEND', () => { + window.chibi.openFrontend(); + }); + xmlList.push(dashboardButton); + + // Add load from url button + const sideloadButton = document.createElement('button'); + sideloadButton.setAttribute('text', format('chibi.sideload')); + sideloadButton.setAttribute('callbackKey', 'CHIBI_SIDELOAD_FROM_URL'); + workspace.registerButtonCallback('CHIBI_SIDELOAD_FROM_URL', () => { + const url = prompt(format('chibi.enterURL')); + if (!url) return; + const mode = confirm(format('chibi.loadInSandbox')) + ? 'sandboxed' + : 'unsandboxed'; + window.chibi.loader.load(url, mode); + }); + xmlList.push(sideloadButton); + + // Add chibi detection + const mutation = document.createElement('mutation'); + mutation.setAttribute('chibi', 'installed'); + const field = document.createElement('field'); + field.setAttribute('name', 'VALUE'); + field.innerHTML = '🧐 Chibi?'; + const block = document.createElement('block'); + block.setAttribute('type', 'argument_reporter_boolean'); + block.setAttribute('gap', '16'); + block.appendChild(field); + block.appendChild(mutation); + xmlList.push(block); + return xmlList; + }; + const workspace = window.Blockly.getMainWorkspace(); + workspace.getToolbox().refreshSelection(); + workspace.toolboxRefreshEnabled_ = true; + return; + } const originalAddCreateButton_ = blockly.Procedures.addCreateButton_; blockly.Procedures.addCreateButton_ = function ( workspace: ChibiCompatibleWorkspace, @@ -272,10 +337,7 @@ export function inject(vm: ChibiCompatibleVM) { // Add dashboard button const dashboardButton = document.createElement('button'); - dashboardButton.setAttribute( - 'text', - lang.format('chibi.openFrontend') - ); + dashboardButton.setAttribute('text', format('chibi.openFrontend')); dashboardButton.setAttribute('callbackKey', 'CHIBI_FRONTEND'); workspace.registerButtonCallback('CHIBI_FRONTEND', () => { window.chibi.openFrontend(); @@ -284,17 +346,12 @@ export function inject(vm: ChibiCompatibleVM) { // Add load from url button const sideloadButton = document.createElement('button'); - sideloadButton.setAttribute('text', lang.format('chibi.sideload')); - sideloadButton.setAttribute( - 'callbackKey', - 'CHIBI_SIDELOAD_FROM_URL' - ); + sideloadButton.setAttribute('text', format('chibi.sideload')); + sideloadButton.setAttribute('callbackKey', 'CHIBI_SIDELOAD_FROM_URL'); workspace.registerButtonCallback('CHIBI_SIDELOAD_FROM_URL', () => { - const url = prompt('Enter URL'); + const url = prompt(format('chibi.enterURL')); if (!url) return; - const mode = confirm(lang.format('chibi.loadInSandbox')) - ? 'sandboxed' - : 'unsandboxed'; + const mode = confirm(format('chibi.loadInSandbox')) ? 'sandboxed' : 'unsandboxed'; window.chibi.loader.load(url, mode); }); xmlList.push(sideloadButton); diff --git a/src/l10n/l10n.json b/src/l10n/l10n.json new file mode 100644 index 0000000..8388f31 --- /dev/null +++ b/src/l10n/l10n.json @@ -0,0 +1,29 @@ +{ + "zh-cn": { + "chibi.openFrontend": "打开面板", + "chibi.sideload": "从 URL 侧载扩展", + "chibi.errorIgnored": "在加载扩展扩展时出现错误。为了避免加载进程的中断,此错误已被忽略。", + "chibi.tryLoad": "🤨 项目正从 [url] 加载扩展 [extensionURL]。要加载么?", + "chibi.tryLoadInEnv": "🤨 项目正以 [env] 模式从 [url] 加载扩展 [extensionURL]。要加载么?", + "chibi.loadInSandbox": "🤨 要在沙箱模式中加载扩展么?", + "chibi.enterURL": "🌐 输入" + }, + "en": { + "chibi.openFrontend": "Open Frontend", + "chibi.sideload": "Sideload from URL", + "chibi.errorIgnored": "Error occurred while sideloading extension. To avoid interrupting the loading process, we chose to ignore this error.", + "chibi.tryLoad": "🤨 Project is trying to sideloading [extensionURL] from [url]. Do you want to load?", + "chibi.tryLoadInEnv": "🤨 Project is trying to sideloading [extensionURL] from [url] in [env] mode. Do you want to load?", + "chibi.loadInSandbox": "🤨 Do you want to load it in the sandbox?", + "chibi.enterURL": "🌐 Enter URL" + }, + "ja": { + "chibi.openFrontend": "ダッシュボードを開く", + "chibi.sideload": "URL から拡張機能を導入", + "chibi.errorIgnored": "拡張機能のサイドロード中でエラーが発生しました。ロードの中断を防ぐために、このエラーは無視しました。", + "chibi.tryLoad": "🤨 プロジェクトは [url] から [extensionURL] をサイドロードしています。ロードしますか?", + "chibi.tryLoadInEnv": "🤨 プロジェクトは [env] モードで、[url] から [extensionURL] をサイドロードしています。ロードしますか?", + "chibi.loadInSandbox": "🤨 サンドボックス環境でロードしますか?", + "chibi.enterURL": "🌐 URL を入力してください。" + } +} diff --git a/src/l10n/lang.ts b/src/l10n/lang.ts deleted file mode 100644 index c3d9ea7..0000000 --- a/src/l10n/lang.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { warn } from '../util/log'; -const l10n: Record> = { - 'zh-cn': { - 'chibi.openFrontend': '打开面板', - 'chibi.sideload': '从 URL 侧载扩展', - 'chibi.errorIgnored': - '在加载扩展扩展时出现错误。为了避免加载进程的中断,此错误已被忽略。', - 'chibi.tryLoad': '🤨 项目正从 {URL} 加载扩展 {EXT_URL}。要加载么?', - 'chibi.tryLoadInEnv': - '🤨 项目正以 {ENV} 模式从 {URL} 加载扩展 {EXT_URL}。要加载么?', - 'chibi.loadInSandbox': '🤨 要在沙箱模式中加载扩展么?', - 'chibi.enterURL': '🌐 输入', - }, - en: { - 'chibi.openFrontend': 'Open Frontend', - 'chibi.sideload': 'Sideload from URL', - 'chibi.errorIgnored': - 'Error occurred while sideloading extension. To avoid interrupting the loading process, we chose to ignore this error.', - 'chibi.tryLoad': - '🤨 Project is trying to sideloading {EXT_URL} from {URL}. Do you want to load?', - 'chibi.tryLoadInEnv': - '🤨 Project is trying to sideloading {EXT_URL} from {URL} in {ENV} mode. Do you want to load?', - 'chibi.loadInSandbox': '🤨 Do you want to load it in the sandbox?', - 'chibi.enterURL': '🌐 Enter URL', - }, - ja: { - 'chibi.openFrontend': 'ダッシュボードを開く', - 'chibi.sideload': 'URL から拡張機能を導入', - 'chibi.errorIgnored': - '拡張機能のサイドロード中でエラーが発生しました。ロードの中断を防ぐために、このエラーは無視しました。', - 'chibi.tryLoad': - '🤨 プロジェクトは {URL} から {EXT_URL} をサイドロードしています。ロードしますか?', - 'chibi.tryLoadInEnv': - '🤨 プロジェクトは {ENV} モードで、{URL} から {EXT_URL} をサイドロードしています。ロードしますか?', - 'chibi.loadInSandbox': '🤨 サンドボックス環境でロードしますか?', - 'chibi.enterURL': '🌐 URL を入力してください。', - }, -}; -class Language { - lang: string; - constructor(lang: string) { - this.lang = lang; - } - format(id: string) { - if (l10n[this.lang] && id in l10n[this.lang]) { - return l10n[this.lang][id]; - } else if (l10n['en'] && id in l10n['en']) { - return l10n['en'][id]; - } - return id; - } -} -export default function setup(lang: string): Language { - if (!(lang in l10n)) { - warn( - '🥺 This language is currently not supported. Falling back to English.' - ); - } - return new Language(lang); -} diff --git a/src/loader/dispatch/central-dispatch.ts b/src/loader/dispatch/central-dispatch.ts index 40e5406..db3b74b 100644 --- a/src/loader/dispatch/central-dispatch.ts +++ b/src/loader/dispatch/central-dispatch.ts @@ -12,8 +12,7 @@ class _CentralDispatch extends SharedDispatch { * The constructor we will use to recognize workers. * @type {Worker | null} */ - workerClass: typeof Worker | null = - typeof Worker === 'undefined' ? null : Worker; + workerClass: typeof Worker | null = typeof Worker === 'undefined' ? null : Worker; /** * List of workers attached to this dispatcher. * @type {Array} @@ -43,9 +42,7 @@ class _CentralDispatch extends SharedDispatch { const { provider, isRemote } = this._getServiceProvider(service); if (provider) { if (isRemote) { - throw new Error( - `Cannot use 'callSync' on remote provider for service ${service}.` - ); + throw new Error(`Cannot use 'callSync' on remote provider for service ${service}.`); } return provider[method].apply(provider, args); } @@ -59,9 +56,7 @@ class _CentralDispatch extends SharedDispatch { */ setServiceSync(service: string, provider: any) { if (this.services.hasOwnProperty(service)) { - console.warn( - `Central dispatch replacing existing service provider for ${service}` - ); + console.warn(`Central dispatch replacing existing service provider for ${service}`); } this.services[service] = provider; } @@ -94,9 +89,7 @@ class _CentralDispatch extends SharedDispatch { console.error(`Could not handshake with worker: ${e}`); }); } else { - console.warn( - 'Central dispatch ignoring attempt to add duplicate worker' - ); + console.warn('Central dispatch ignoring attempt to add duplicate worker'); } } /** @@ -112,10 +105,8 @@ class _CentralDispatch extends SharedDispatch { provider && { provider, isRemote: Boolean( - (this.workerClass && - provider instanceof this.workerClass) || - provider.isRemote - ), + (this.workerClass && provider instanceof this.workerClass) || provider.isRemote + ) } ); } diff --git a/src/loader/dispatch/shared-dispatch.ts b/src/loader/dispatch/shared-dispatch.ts index 3ac76ca..305734f 100644 --- a/src/loader/dispatch/shared-dispatch.ts +++ b/src/loader/dispatch/shared-dispatch.ts @@ -91,24 +91,13 @@ class SharedDispatch { * @param {*} [args] - the arguments to be copied to the method, if any. * @returns {Promise} - a promise for the return value of the service method. */ - transferCall( - service: string, - method: string, - transfer: unknown, - ...args: unknown[] - ) { + transferCall(service: string, method: string, transfer: unknown, ...args: unknown[]) { try { // @ts-expect-error TS(2339): It's not implemented. const { provider, isRemote } = this._getServiceProvider(service); if (provider) { if (isRemote) { - return this._remoteTransferCall( - provider, - service, - method, - transfer, - ...args - ); + return this._remoteTransferCall(provider, service, method, transfer, ...args); } const result = provider[method](...args); return Promise.resolve(result); @@ -142,13 +131,7 @@ class SharedDispatch { method: string, ...args: unknown[] ): Promise { - return this._remoteTransferCall( - provider, - service, - method, - null, - ...args - ); + return this._remoteTransferCall(provider, service, method, null, ...args); } /** * Like {@link transferCall}, but force the call to be posted through a particular communication channel. @@ -172,10 +155,7 @@ class SharedDispatch { args = this._purifyObject(args) as unknown[]; } if (transfer) { - provider.postMessage( - { service, method, responseId, args }, - transfer - ); + provider.postMessage({ service, method, responseId, args }, transfer); } else { provider.postMessage({ service, method, responseId, args }); } @@ -188,10 +168,7 @@ class SharedDispatch { * @returns {*} - a unique response ID for this set of callbacks. See {@link _deliverResponse}. * @protected */ - _storeCallbacks( - resolve: (value: unknown) => void, - reject: (value: unknown) => void - ) { + _storeCallbacks(resolve: (value: unknown) => void, reject: (value: unknown) => void) { const responseId = this.nextResponseId++; this.callbacks[responseId] = [resolve, reject]; return responseId; @@ -230,17 +207,11 @@ class SharedDispatch { if (message.service === 'dispatch') { promise = this._onDispatchMessage(worker, message); } else { - promise = this.call( - message.service, - message.method, - ...message.args - ); + promise = this.call(message.service, message.method, ...message.args); } } else if (typeof message.responseId === 'undefined') { console.error( - `Dispatch caught malformed message from a worker: ${JSON.stringify( - event - )}` + `Dispatch caught malformed message from a worker: ${JSON.stringify(event)}` ); } else { this._deliverResponse(message.responseId, message); @@ -248,21 +219,19 @@ class SharedDispatch { if (promise) { if (typeof message.responseId === 'undefined') { console.error( - `Dispatch message missing required response ID: ${JSON.stringify( - event - )}` + `Dispatch message missing required response ID: ${JSON.stringify(event)}` ); } else { promise.then( (result) => worker.postMessage({ responseId: message.responseId, - result, + result }), (error) => worker.postMessage({ responseId: message.responseId, - error: `${error}`, + error: `${error}` }) ); } @@ -309,9 +278,7 @@ class SharedDispatch { visited.add(obj); if (Array.isArray(obj)) { - return obj.map((item) => - this._purifyObject(item, visited, depth + 1) - ); + return obj.map((item) => this._purifyObject(item, visited, depth + 1)); } const result: Record = {}; for (const key in obj) { diff --git a/src/loader/dispatch/worker-dispatch.ts b/src/loader/dispatch/worker-dispatch.ts index 6f85e19..a246a95 100644 --- a/src/loader/dispatch/worker-dispatch.ts +++ b/src/loader/dispatch/worker-dispatch.ts @@ -61,9 +61,7 @@ class _WorkerDispatch extends SharedDispatch { */ setService(service: string, provider: unknown) { if (this.services.hasOwnProperty(service)) { - console.warn( - `Worker dispatch replacing existing service provider for ${service}` - ); + console.warn(`Worker dispatch replacing existing service provider for ${service}`); } this.services[service] = provider; return this.waitForConnection.then(() => @@ -83,7 +81,7 @@ class _WorkerDispatch extends SharedDispatch { const provider = this.services[service]; return { provider: provider || self, - isRemote: !provider, + isRemote: !provider }; } diff --git a/src/loader/loader.ts b/src/loader/loader.ts index 31c2625..f9c2ed1 100644 --- a/src/loader/loader.ts +++ b/src/loader/loader.ts @@ -7,7 +7,7 @@ import { ExtensionBlockMetadata, BlockType, MenuItems, - BlockArgs, + BlockArgs } from '../typings'; import { maybeFormatMessage } from '../util/maybe-format-message'; import { CentralDispatch as dispatch } from './dispatch/central-dispatch'; @@ -80,11 +80,7 @@ class ChibiLoader { ); } dispatch.setService('loader', this).catch((e: Error) => { - error( - `ChibiLoader was unable to register extension service: ${JSON.stringify( - e - )}` - ); + error(`ChibiLoader was unable to register extension service: ${JSON.stringify(e)}`); }); } @@ -93,10 +89,7 @@ class ChibiLoader { * @param {ExtensionClass | string} ext - Extension's data. * @param {'sandboxed' | 'unsandboxed'} env - Extension's running environment. */ - async load( - ext: string | ExtensionClass, - env: 'sandboxed' | 'unsandboxed' = 'sandboxed' - ) { + async load(ext: string | ExtensionClass, env: 'sandboxed' | 'unsandboxed' = 'sandboxed') { if (typeof ext === 'string') { switch (env) { case 'sandboxed': @@ -106,7 +99,7 @@ class ChibiLoader { this.pendingExtensions.push({ extensionURL: ext, resolve, - reject, + reject }); dispatch.addWorker(ExtensionWorker); }); @@ -115,15 +108,9 @@ class ChibiLoader { const originalScript = await response.text(); const closureFunc = new Function('Scratch', originalScript); const ctx = makeCtx(this.vm); - ctx.extensions.register = ( - extensionObj: ExtensionClass - ) => { + ctx.extensions.register = (extensionObj: ExtensionClass) => { const extensionInfo = extensionObj.getInfo(); - this._registerExtensionInfo( - extensionObj, - extensionInfo, - ext - ); + this._registerExtensionInfo(extensionObj, extensionInfo, ext); }; closureFunc(ctx); return; @@ -136,11 +123,7 @@ class ChibiLoader { // @ts-expect-error Load as builtin extension. const extensionObject = new ext(this.vm.runtime); const extensionInfo = extensionObject.getInfo() as ExtensionMetadata; - this._registerExtensionInfo( - extensionObject, - extensionInfo, - extensionInfo.id - ); + this._registerExtensionInfo(extensionObject, extensionInfo, extensionInfo.id); return extensionInfo; } @@ -156,11 +139,7 @@ class ChibiLoader { // It's running in worker if (typeof targetExt.instance === 'string') { const info = await dispatch.call(targetExt.instance, 'getInfo'); - const processedInfo = this._prepareExtensionInfo( - null, - info, - targetExt.instance - ); + const processedInfo = this._prepareExtensionInfo(null, info, targetExt.instance); // @ts-expect-error private method this.vm.runtime._refreshExtensionPrimitives(processedInfo); return processedInfo; @@ -230,17 +209,11 @@ class ChibiLoader { id: extensionInfo.id, url: extensionURL, info: extensionInfo, - instance: (extensionObject ?? serviceName) as - | ExtensionClass - | string, - env: serviceName ? 'sandboxed' : 'unsandboxed', + instance: (extensionObject ?? serviceName) as ExtensionClass | string, + env: serviceName ? 'sandboxed' : 'unsandboxed' } as ScratchExtension); } - extensionInfo = this._prepareExtensionInfo( - extensionObject, - extensionInfo, - serviceName - ); + extensionInfo = this._prepareExtensionInfo(extensionObject, extensionInfo, serviceName); // @ts-expect-error private method this.vm.runtime._registerExtensionPrimitives(extensionInfo); @@ -297,9 +270,9 @@ class ChibiLoader { } catch (e: unknown) { // TODO: more meaningful error reporting error( - `Error processing block: ${ - (e as Error).message - }, Block:\n${JSON.stringify(blockInfo)}` + `Error processing block: ${(e as Error).message}, Block:\n${JSON.stringify( + blockInfo + )}` ); } return results; @@ -340,7 +313,7 @@ class ChibiLoader { if (!menuInfo.items) { menuInfo = { // @ts-expect-error - items: menuInfo, + items: menuInfo }; menus[menuName] = menuInfo; } @@ -381,41 +354,29 @@ class ChibiLoader { */ const editingTarget = - this.vm.runtime.getEditingTarget() || - this.vm.runtime.getTargetForStage(); + this.vm.runtime.getEditingTarget() || this.vm.runtime.getTargetForStage(); const editingTargetID = editingTarget ? editingTarget.id : null; // @ts-expect-error private method - const extensionMessageContext = - this.vm.runtime.makeMessageContextForTarget(editingTarget); + const extensionMessageContext = this.vm.runtime.makeMessageContextForTarget(editingTarget); // TODO: Fix this to use dispatch.call when extensions are running in workers. const menuFunc = extensionObject[menuItemFunctionName] as ( editingTargetID: string | null ) => MenuItems; - const menuItems = menuFunc - .call(extensionObject, editingTargetID) - .map((item) => { - item = maybeFormatMessage(item, extensionMessageContext); - switch (typeof item) { - case 'object': - return [ - maybeFormatMessage( - item.text, - extensionMessageContext - ), - item.value, - ]; - case 'string': - return [item, item]; - default: - return item; - } - }); + const menuItems = menuFunc.call(extensionObject, editingTargetID).map((item) => { + item = maybeFormatMessage(item, extensionMessageContext); + switch (typeof item) { + case 'object': + return [maybeFormatMessage(item.text, extensionMessageContext), item.value]; + case 'string': + return [item, item]; + default: + return item; + } + }); if (!menuItems || menuItems.length < 1) { - throw new Error( - `Extension menu returned no items: ${menuItemFunctionName}` - ); + throw new Error(`Extension menu returned no items: ${menuItemFunctionName}`); } return menuItems; } @@ -439,12 +400,11 @@ class ChibiLoader { blockType: BlockType.COMMAND, terminal: false, blockAllThreads: false, - arguments: {}, + arguments: {} }, blockInfo ); - blockInfo.opcode = - blockInfo.opcode && this._sanitizeID(blockInfo.opcode); + blockInfo.opcode = blockInfo.opcode && this._sanitizeID(blockInfo.opcode); blockInfo.text = blockInfo.text || blockInfo.opcode; switch (blockInfo.blockType) { @@ -472,21 +432,13 @@ class ChibiLoader { : blockInfo.opcode; const getBlockInfo = blockInfo.isDynamic - ? (args: BlockArgs) => - args && args.mutation && args.mutation.blockInfo + ? (args: BlockArgs) => args && args.mutation && args.mutation.blockInfo : () => blockInfo; const callBlockFunc = (() => { // Maybe there's a worker if (extensionObject === null) { - if ( - serviceName && - dispatch._isRemoteService(serviceName) - ) { - return ( - args: BlockArgs, - _util: unknown, - realBlockInfo: unknown - ) => + if (serviceName && dispatch._isRemoteService(serviceName)) { + return (args: BlockArgs, _util: unknown, realBlockInfo: unknown) => dispatch.call( serviceName, funcName, @@ -495,24 +447,16 @@ class ChibiLoader { realBlockInfo ); } - warn( - `Could not find extension block function called ${funcName}` - ); + warn(`Could not find extension block function called ${funcName}`); // eslint-disable-next-line @typescript-eslint/no-empty-function return () => {}; } if (!extensionObject[funcName]) { // The function might show up later as a dynamic property of the service object - warn( - `Could not find extension block function called ${funcName}` - ); + warn(`Could not find extension block function called ${funcName}`); } - return ( - args: BlockArgs, - util: unknown, - realBlockInfo: unknown - ) => + return (args: BlockArgs, util: unknown, realBlockInfo: unknown) => // @ts-expect-error extensionObject[funcName](args, util, realBlockInfo); })(); diff --git a/src/loader/make-ctx.ts b/src/loader/make-ctx.ts index 906d1ae..298db22 100644 --- a/src/loader/make-ctx.ts +++ b/src/loader/make-ctx.ts @@ -3,7 +3,7 @@ import { TargetType, ArgumentType, ReporterScope, - StandardScratchExtensionClass as ExtensionClass, + StandardScratchExtensionClass as ExtensionClass } from '../typings'; import { Cast } from '../util/cast'; import formatMessage, { Message } from 'format-message'; @@ -26,7 +26,7 @@ export interface Context { renderer?: Renderer; } /** - * i10n support for Chibi extensions. + * I10n support for Chibi extensions. * @param vm Virtual machine instance. Optional. * @returns Something like Scratch.translate. */ @@ -38,7 +38,7 @@ function createTranslate(vm?: VM) { // Already in the expected format } else if (typeof message === 'string') { message = { - default: message, + default: message }; } else { throw new Error('unsupported data type in translate()'); @@ -64,7 +64,7 @@ function createTranslate(vm?: VM) { locale: getLocale(), missingTranslation: 'ignore', generateId, - translations: storedTranslations, + translations: storedTranslations }); }; @@ -96,9 +96,9 @@ export function makeCtx(vm?: VM) { throw new Error('not implemented'); }, unsandboxed: !!vm, - chibi: true, + chibi: true }, - translate: createTranslate(vm), + translate: createTranslate(vm) }; if (vm) { ctx.vm = vm; diff --git a/src/loader/sandbox.worker.ts b/src/loader/sandbox.worker.ts index 6330f3d..d1a7378 100644 --- a/src/loader/sandbox.worker.ts +++ b/src/loader/sandbox.worker.ts @@ -47,12 +47,7 @@ class ExtensionWorker { const promise = dispatch .setService(serviceName, extensionObject) .then(() => - dispatch.call( - 'loader', - 'registerExtensionService', - this.extensionURL, - serviceName - ) + dispatch.call('loader', 'registerExtensionService', this.extensionURL, serviceName) ); if (this.initialRegistrations) { this.initialRegistrations.push(promise); @@ -67,7 +62,6 @@ globalThis.Scratch = makeCtx(); * Expose only specific parts of the worker to extensions. */ const extensionWorker = new ExtensionWorker(); -globalThis.Scratch.extensions.register = - extensionWorker.register.bind(extensionWorker); +globalThis.Scratch.extensions.register = extensionWorker.register.bind(extensionWorker); export default null as unknown as any; diff --git a/src/typings/argument-type.ts b/src/typings/argument-type.ts index 9b79979..a931780 100644 --- a/src/typings/argument-type.ts +++ b/src/typings/argument-type.ts @@ -34,7 +34,7 @@ enum ArgumentType { /** * Inline image on block (as part of the label) */ - IMAGE = 'image', + IMAGE = 'image' } export { ArgumentType }; diff --git a/src/typings/block-type.ts b/src/typings/block-type.ts index a810959..76382c1 100644 --- a/src/typings/block-type.ts +++ b/src/typings/block-type.ts @@ -41,7 +41,7 @@ enum BlockType { /** * Lambda reporter with callable function. */ - Lambda = 'Lambda', + Lambda = 'Lambda' } export { BlockType }; diff --git a/src/typings/reporter-scope.ts b/src/typings/reporter-scope.ts index 2364721..5a2fa1a 100644 --- a/src/typings/reporter-scope.ts +++ b/src/typings/reporter-scope.ts @@ -12,7 +12,7 @@ enum ReporterScope { * This reporter's value is specific to a particular target/sprite. * Another target may have a different value or may not even have a value. */ - TARGET = 'target', + TARGET = 'target' } export { ReporterScope }; diff --git a/src/typings/target-type.ts b/src/typings/target-type.ts index 0009163..bc18e15 100644 --- a/src/typings/target-type.ts +++ b/src/typings/target-type.ts @@ -10,6 +10,6 @@ enum TargetType { /** * Rendered target which cannot move but can change backdrops */ - STAGE = 'stage', + STAGE = 'stage' } export { TargetType }; diff --git a/src/util/cast.ts b/src/util/cast.ts index 103d0d8..c91141f 100644 --- a/src/util/cast.ts +++ b/src/util/cast.ts @@ -84,11 +84,7 @@ class Cast { } if (typeof value === 'string') { // These specific strings are treated as false in Scratch. - if ( - value === '' || - value === '0' || - value.toLowerCase() === 'false' - ) { + if (value === '' || value === '0' || value.toLowerCase() === 'false') { return false; } // All other strings treated as true. @@ -141,9 +137,7 @@ class Cast { * @return {boolean} True if the argument is all white spaces or null / empty. */ static isWhiteSpace(val: unknown) { - return ( - val === null || (typeof val === 'string' && val.trim().length === 0) - ); + return val === null || (typeof val === 'string' && val.trim().length === 0); } /** @@ -176,10 +170,7 @@ class Cast { return 0; } // Handle the special case of Infinity - if ( - (n1 === Infinity && n2 === Infinity) || - (n1 === -Infinity && n2 === -Infinity) - ) { + if ((n1 === Infinity && n2 === Infinity) || (n1 === -Infinity && n2 === -Infinity)) { return 0; } // Compare as numbers. diff --git a/src/util/color.ts b/src/util/color.ts index b8249fb..3ca1df8 100644 --- a/src/util/color.ts +++ b/src/util/color.ts @@ -79,7 +79,7 @@ class Color { return { r: (parsed >> 16) & 0xff, g: (parsed >> 8) & 0xff, - b: parsed & 0xff, + b: parsed & 0xff }; } else if (hex.length === 3) { const r = (parsed >> 8) & 0xf; @@ -88,7 +88,7 @@ class Color { return { r: (r << 4) | r, g: (g << 4) | g, - b: (b << 4) | b, + b: (b << 4) | b }; } return null; @@ -180,7 +180,7 @@ class Color { return { r: Math.floor(r * 255), g: Math.floor(g * 255), - b: Math.floor(b * 255), + b: Math.floor(b * 255) }; } @@ -223,7 +223,7 @@ class Color { return { r: fraction0 * rgb0.r + fraction1 * rgb1.r, g: fraction0 * rgb0.g + fraction1 * rgb1.g, - b: fraction0 * rgb0.b + fraction1 * rgb1.b, + b: fraction0 * rgb0.b + fraction1 * rgb1.b }; } } diff --git a/src/util/maybe-format-message.ts b/src/util/maybe-format-message.ts index fbedb31..e060710 100644 --- a/src/util/maybe-format-message.ts +++ b/src/util/maybe-format-message.ts @@ -7,11 +7,7 @@ import formatMessage from 'format-message'; * @param {string} [locale] - the locale to pass to `formatMessage` if it gets called. * @return {string|*} - the formatted message OR the original `maybeMessage` input. */ -export const maybeFormatMessage = function ( - maybeMessage?: any, - args?: any, - locale?: any -) { +export const maybeFormatMessage = function (maybeMessage?: any, args?: any, locale?: any) { if (maybeMessage && maybeMessage.id && maybeMessage.default) { return formatMessage(maybeMessage, args, locale); } diff --git a/tsconfig.json b/tsconfig.json index eff8b88..aba4843 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -42,7 +42,7 @@ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ + "resolveJsonModule": true, /* Enable importing .json files. */ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */