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
new file mode 100644
index 0000000..5855764
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,10 @@
+{
+ "semi": true,
+ "tabWidth": 4,
+ "singleQuote": true,
+ "trailingComma": "none",
+ "arrowParens": "always",
+ "bracketSameLine": true,
+ "bracketSpacing": true,
+ "printWidth": 100
+}
diff --git a/README-ja_JP.md b/README-ja_JP.md
new file mode 100644
index 0000000..6b331de
--- /dev/null
+++ b/README-ja_JP.md
@@ -0,0 +1,55 @@
+
+
+
+
+# Chibi (チビ)
+
+#### とあるユニバーサル Scratch 拡張機能ローダーです。
+
+
+
+---
+
+Chibi は Tampermonkey/GreasyMonkey に対応するユーザースクリプトであり、「どのエディターでも拡張機能が使える」という仕様が搭載されています(理論的に)。
+
+# ✨ 仕様
+
+- [X] 標準 Scratch 拡張機能をロードできる
+- [X] Unsandboxed 拡張機能をロードできる
+- [X] どこでも Turbowarp 専用機能を使える (一部)
+- [ ] 拡張機能をインストールしなくでもサードパーティー拡張機能を搭載したプロジェクトを起動できる
+- [X] 直接的にエディターから拡張機能をロードできる
+
+# 🌈 プラットフォーム
+
+- [X] Scratch
+- [X] CodingClip
+- [X] Cocrea
+- [X] Aerfaying
+- [X] Co-Create World
+- [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..75349ca 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,7 +19,11 @@ 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()) {
processedExtInfo.push({
@@ -29,39 +33,65 @@ function getExtensionInfo () {
}
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;
+ // Handshake: send current extension info in order to prepare frontend.
+ 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':
+ // Load an 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..2302879 100644
--- a/src/injector/inject.ts
+++ b/src/injector/inject.ts
@@ -1,57 +1,81 @@
///
-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 * as l10n from '../l10n/l10n.json';
+import formatMessage from 'format-message';
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 +87,8 @@ 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];
@@ -78,15 +102,51 @@ 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 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[]
+ ) {
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(
+ format('chibi.tryLoadInEnv', {
+ extensionURL,
+ url,
+ env
+ })
+ )
+ : confirm(
+ format('chibi.tryLoadInEnv', {
+ extensionURL,
+ url
+ })
+ );
+ if (res) {
+ await loader.load(
+ url,
+ (env
+ ? env
+ : 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');
@@ -95,7 +155,7 @@ export function inject (vm: ChibiCompatibleVM) {
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(format('chibi.errorIgnored'), e);
}
} else {
// @ts-expect-error internal hack
@@ -121,15 +181,17 @@ 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[]) {
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,28 +201,43 @@ export function inject (vm: ChibiCompatibleVM) {
const originSetLocaleFunc = vm.setLocale;
vm.setLocale = function (locale: string, ...args: unknown[]) {
+ 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
vm.emit('LOCALE_CHANGED', locale);
return result;
};
-
+ // TODO: compiler support
const originalArgReporterBooleanFunc = vm.runtime._primitives['argument_reporter_boolean'];
- vm.runtime._primitives['argument_reporter_boolean'] = function (args: Record, ...otherArgs: unknown[]) {
+ 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 Installed?':
+ warn("'🧐 Chibi Installed?' is deprecated, use '🧐 Chibi?' instead.");
+ return true;
+ 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[]) {
+ vm.ccExtensionManager.getExtensionLoadOrder = function (
+ extensions: string[],
+ ...args: unknown[]
+ ) {
for (const extensionId of extensions) {
if (
!vm.ccExtensionManager!.info.hasOwnProperty(extensionId) &&
@@ -178,10 +255,12 @@ export function inject (vm: ChibiCompatibleVM) {
// Blockly stuffs
setTimeout(() => {
- const blockly = window.chibi.blockly = getBlocklyInstance();
+ 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;
+ const originalProcedureCallback =
+ window.Blockly?.getMainWorkspace().toolboxCategoryCallbacks_.PROCEDURE;
if (!originalProcedureCallback) {
error('alternative method failed, stop injecting');
return;
@@ -201,7 +280,7 @@ export function inject (vm: ChibiCompatibleVM) {
// Add dashboard button
const dashboardButton = document.createElement('button');
- dashboardButton.setAttribute('text', 'Open Frontend');
+ dashboardButton.setAttribute('text', format('chibi.openFrontend'));
dashboardButton.setAttribute('callbackKey', 'CHIBI_FRONTEND');
workspace.registerButtonCallback('CHIBI_FRONTEND', () => {
window.chibi.openFrontend();
@@ -210,12 +289,14 @@ export function inject (vm: ChibiCompatibleVM) {
// Add load from url button
const sideloadButton = document.createElement('button');
- sideloadButton.setAttribute('text', '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('Running in sandbox?') ? 'sandboxed' : 'unsandboxed';
+ const mode = confirm(format('chibi.loadInSandbox'))
+ ? 'sandboxed'
+ : 'unsandboxed';
window.chibi.loader.load(url, mode);
});
xmlList.push(sideloadButton);
@@ -225,7 +306,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');
@@ -238,8 +319,7 @@ export function inject (vm: ChibiCompatibleVM) {
workspace.getToolbox().refreshSelection();
workspace.toolboxRefreshEnabled_ = true;
return;
- };
-
+ }
const originalAddCreateButton_ = blockly.Procedures.addCreateButton_;
blockly.Procedures.addCreateButton_ = function (
workspace: ChibiCompatibleWorkspace,
@@ -257,7 +337,7 @@ export function inject (vm: ChibiCompatibleVM) {
// Add dashboard button
const dashboardButton = document.createElement('button');
- dashboardButton.setAttribute('text', 'Open Frontend');
+ dashboardButton.setAttribute('text', format('chibi.openFrontend'));
dashboardButton.setAttribute('callbackKey', 'CHIBI_FRONTEND');
workspace.registerButtonCallback('CHIBI_FRONTEND', () => {
window.chibi.openFrontend();
@@ -266,12 +346,12 @@ export function inject (vm: ChibiCompatibleVM) {
// Add load from url button
const sideloadButton = document.createElement('button');
- sideloadButton.setAttribute('text', '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('Running in sandbox?') ? 'sandboxed' : 'unsandboxed';
+ const mode = confirm(format('chibi.loadInSandbox')) ? 'sandboxed' : 'unsandboxed';
window.chibi.loader.load(url, mode);
});
xmlList.push(sideloadButton);
@@ -281,7 +361,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/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/loader/dispatch/central-dispatch.ts b/src/loader/dispatch/central-dispatch.ts
index 261203e..db3b74b 100644
--- a/src/loader/dispatch/central-dispatch.ts
+++ b/src/loader/dispatch/central-dispatch.ts
@@ -12,14 +12,14 @@ 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,8 +38,8 @@ 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}.`);
@@ -54,7 +54,7 @@ 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}`);
}
@@ -67,7 +67,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,11 +81,11 @@ 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 {
@@ -99,12 +99,16 @@ 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 +118,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..305734f 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,10 +91,10 @@ 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);
@@ -113,7 +113,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,7 +125,12 @@ 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 {
+ _remoteCall(
+ provider: any,
+ service: string,
+ method: string,
+ ...args: unknown[]
+ ): Promise {
return this._remoteTransferCall(provider, service, method, null, ...args);
}
/**
@@ -137,16 +142,22 @@ 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 +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;
@@ -168,7 +179,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 +198,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 || [];
@@ -199,15 +210,30 @@ class SharedDispatch {
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 +244,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 +257,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 +268,18 @@ 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));
- }
+ }
const result: Record = {};
for (const key in obj) {
// @ts-expect-error
@@ -257,12 +287,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..a246a95 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,14 @@ 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}`);
}
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,7 +76,7 @@ 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 {
@@ -94,19 +93,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..f9c2ed1 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,
@@ -9,16 +9,14 @@ import {
MenuItems,
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,13 +69,15 @@ 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)}`);
@@ -89,34 +89,34 @@ 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');
}
}
@@ -131,7 +131,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}.`);
@@ -154,7 +154,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 +164,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 +178,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 +193,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.`);
@@ -220,7 +225,7 @@ class ChibiLoader {
* @returns {string} - the sanitized text
* @private
*/
- private _sanitizeID (text: string) {
+ private _sanitizeID(text: string) {
return text.toString().replace(/[<"&]/, '_');
}
@@ -233,7 +238,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 +250,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 +296,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];
@@ -296,7 +324,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,34 +343,37 @@ 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);
// 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) {
+ 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
- ];
+ 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}`);
@@ -353,72 +389,92 @@ 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);
+ 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 +482,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 +501,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 +511,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 +522,4 @@ class ChibiLoader {
}
}
-export {
- ChibiLoader
-};
+export { ChibiLoader };
diff --git a/src/loader/make-ctx.ts b/src/loader/make-ctx.ts
index 8d20776..298db22 100644
--- a/src/loader/make-ctx.ts
+++ b/src/loader/make-ctx.ts
@@ -10,7 +10,7 @@ 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) => {
@@ -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';
};
@@ -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,
diff --git a/src/loader/sandbox.worker.ts b/src/loader/sandbox.worker.ts
index 80d63ce..d1a7378 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,15 @@ 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);
}
diff --git a/src/typings/argument-type.ts b/src/typings/argument-type.ts
index 41c73e5..a931780 100644
--- a/src/typings/argument-type.ts
+++ b/src/typings/argument-type.ts
@@ -37,6 +37,4 @@ enum ArgumentType {
IMAGE = 'image'
}
-export {
- ArgumentType
-};
+export { ArgumentType };
diff --git a/src/typings/block-type.ts b/src/typings/block-type.ts
index 12ecc1e..76382c1 100644
--- a/src/typings/block-type.ts
+++ b/src/typings/block-type.ts
@@ -44,6 +44,4 @@ enum BlockType {
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..5a2fa1a 100644
--- a/src/typings/reporter-scope.ts
+++ b/src/typings/reporter-scope.ts
@@ -15,6 +15,4 @@ enum ReporterScope {
TARGET = 'target'
}
-export {
- ReporterScope
-};
+export { ReporterScope };
diff --git a/src/typings/target-type.ts b/src/typings/target-type.ts
index bdc3c30..bc18e15 100644
--- a/src/typings/target-type.ts
+++ b/src/typings/target-type.ts
@@ -12,6 +12,4 @@ enum TargetType {
*/
STAGE = 'stage'
}
-export {
- TargetType
-};
+export { TargetType };
diff --git a/src/util/cast.ts b/src/util/cast.ts
index a322a78..c91141f 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,14 @@ 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 +99,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 +108,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 +118,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,7 +136,7 @@ class Cast {
* @param {*} val value to check.
* @return {boolean} True if the argument is all white spaces or null / empty.
*/
- static isWhiteSpace (val: unknown) {
+ static isWhiteSpace(val: unknown) {
return val === null || (typeof val === 'string' && val.trim().length === 0);
}
@@ -149,7 +147,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)) {
@@ -172,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.
@@ -187,10 +182,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 +201,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 +219,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 +244,4 @@ class Cast {
}
}
-export {
- Cast
-};
+export { Cast };
diff --git a/src/util/color.ts b/src/util/color.ts
index 879498b..3ca1df8 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);
}
@@ -82,8 +82,8 @@ class Color {
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,
@@ -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,54 +127,54 @@ 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 {
@@ -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/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. */
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"