Skip to content

Commit

Permalink
🔧 chore: fix clipcc support && turbowarp backpack support (#16)
Browse files Browse the repository at this point in the history
* 🐛 fix: clipcc support

* 🎨 chore: add newline

* 🍺 fix: regenrate yarn.lock

Signed-off-by: SimonShiki <sinangentoo@gmail.com>

* 💩 chore: remove Typescript errors

* 🎨 refactor: suppress Typescript errors

* 🔥 chore: fix gitignore

---------

Signed-off-by: FurryR <awathefox@gmail.com>
  • Loading branch information
SimonShiki authored Jan 22, 2024
1 parent 93807ef commit 3fa1eab
Show file tree
Hide file tree
Showing 12 changed files with 1,024 additions and 924 deletions.
2 changes: 1 addition & 1 deletion generate-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const includeURLs = [
'https://play.creaticode.com/projects/*',
'https://www.adacraft.org/*',
'https://studio.penguinmod.com/*',
'https://code.xueersi.com/home/project/detail?lang=scratch&pid=*&version=3.0&langType=scratch',
'https://code.xueersi.com/*',
'http://localhost:8601/*'
];

Expand Down
2 changes: 2 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable multiline-comment-style */
/* eslint-disable @typescript-eslint/triple-slash-reference */
/// <reference path="node_modules/@turbowarp/types/index.d.ts" />
/// <reference path="./loader/loader" />
/// <reference path="./loader/make-ctx" />
Expand Down
231 changes: 134 additions & 97 deletions src/injector/inject.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../global.d.ts" />
import { log, warn, error } from '../util/log';
import { settings } from '../util/settings';
Expand All @@ -7,10 +8,9 @@ import type VM from 'scratch-vm';
import type Blockly from 'scratch-blocks';
import * as l10n from '../l10n/l10n.json';
import formatMessage from 'format-message';
import { makeCtx } from '../loader/make-ctx';

interface EurekaCompatibleWorkspace extends Blockly.Workspace {
registerButtonCallback(key: string, callback: Function): void;
registerButtonCallback(key: string, callback: () => void): void;
}

interface EurekaCompatibleVM extends VM {
Expand Down Expand Up @@ -49,38 +49,64 @@ function getExtensionIdForOpcode (opcode: string) {
* @param vm Virtual machine instance. For some reasons we cannot use VM here.
* @returns Blockly instance.
*/
function getBlocklyInstance (vm: EurekaCompatibleVM): any | null {
// Hijack Function.prototype.apply to get React element instance.
function hijack (fn: (...args: unknown[]) => unknown) {
const _orig = Function.prototype.apply;
Function.prototype.apply = function (thisArg: any) {
return thisArg;
};
const result = fn();
Function.prototype.apply = _orig;
return result;
}
async function getBlocklyInstance (vm: EurekaCompatibleVM): Promise<any> {
function getBlocklyInstanceInternal (): any | null {
// Hijack Function.prototype.apply to get React element instance.
function hijack (fn: (...args: unknown[]) => unknown) {
const _orig = Function.prototype.apply;
Function.prototype.apply = function (thisArg: any) {
return thisArg;
};
const result = fn();
Function.prototype.apply = _orig;
return result;
}

// @ts-expect-error lazy to extend VM interface
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 && typeof result === 'object' && 'ScratchBlocks' in result) {
return result.ScratchBlocks;
}
} else {
// It is an array, hijack every listeners.
for (const value of events) {
const result = hijack(value);
// @ts-expect-error lazy to extend VM interface
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 && typeof result === 'object' && 'ScratchBlocks' in result) {
return result.ScratchBlocks;
}
} else {
// It is an array, hijack every listeners.
for (const value of events) {
const result = hijack(value);
if (result && typeof result === 'object' && 'ScratchBlocks' in result) {
return result.ScratchBlocks;
}
}
}
}
return null;
}
return null;
let res = getBlocklyInstanceInternal();
return (
res ??
new Promise((resolve) => {
let state: any = undefined;
// @ts-expect-error lazy to extend VM interface
Reflect.defineProperty(vm._events, 'EXTENSION_ADDED', {
get: () => state,
set (v) {
state = v;
res = getBlocklyInstanceInternal();
if (res) {
// @ts-expect-error lazy to extend VM interface
Reflect.defineProperty(vm._events, 'EXTENSION_ADDED', {
value: state,
writable: true
});
resolve(res);
}
},
configurable: true
});
})
);
}

/**
Expand Down Expand Up @@ -141,10 +167,7 @@ export function inject (vm: EurekaCompatibleVM) {
generateId: (defaultMessage: string) => `${defaultMessage}`,
translations: l10n
});
vm.extensionManager.loadExtensionURL = async function (
extensionURL: string,
...args: unknown[]
) {
vm.extensionManager.loadExtensionURL = async function (extensionURL: string, ...args: []) {
if (extensionURL in window.eureka.registeredExtension) {
const { url, env } = window.eureka.registeredExtension[extensionURL];
try {
Expand All @@ -160,14 +183,14 @@ export function inject (vm: EurekaCompatibleVM) {
env
})
)
: (window.eureka.settings.sideloadOnly ?
false :
confirm(
: window.eureka.settings.sideloadOnly
? false
: confirm(
format('eureka.tryLoad', {
extensionURL,
url
})
));
);
}
if (whetherSideload) {
await loader.load(
Expand All @@ -178,33 +201,26 @@ export function inject (vm: EurekaCompatibleVM) {
? 'sandboxed'
: 'unsandboxed') as 'unsandboxed' | 'sandboxed'
);
const extensionId = loader.getIdByUrl(url);
// @ts-expect-error internal hack
vm.extensionManager._loadedExtensions.set(extensionId, 'Eureka');
} else {
// @ts-expect-error internal hack
return originalLoadFunc.call(this, extensionURL, ...args);
}
} catch (e: unknown) {
error(format('eureka.errorIgnored'), e);
}
} else {
// @ts-expect-error internal hack
return originalLoadFunc.call(this, extensionURL, ...args);
}
};

const originalRefreshBlocksFunc = vm.extensionManager.refreshBlocks;
vm.extensionManager.refreshBlocks = async function (...args: unknown[]) {
// @ts-expect-error internal hack
vm.extensionManager.refreshBlocks = async function (...args) {
const result = await originalRefreshBlocksFunc.call(this, ...args);
await window.eureka.loader.refreshBlocks();
return result;
};

const originalToJSONFunc = vm.toJSON;
vm.toJSON = function (optTargetId: string, ...args: unknown[]) {
// @ts-expect-error internal hack
vm.toJSON = function (optTargetId: string, ...args) {
const json = originalToJSONFunc.call(this, optTargetId, ...args);
const obj = JSON.parse(json);

Expand Down Expand Up @@ -254,49 +270,55 @@ export function inject (vm: EurekaCompatibleVM) {
};

const originalDrserializeFunc = vm.deserializeProject;
vm.deserializeProject = function (projectJSON: Record<string, any>, ...args: unknown[]) {
vm.deserializeProject = function (projectJSON: Record<string, unknown>, ...args) {
if (typeof projectJSON.extensionURLs === 'object') {
for (const id in projectJSON.extensionURLs) {
const extensionURLs = projectJSON.extensionURLs as Record<string, unknown>;
const extensionEnvs: Record<string, unknown> =
typeof projectJSON.extensionEnvs === 'object'
? (projectJSON.extensionEnvs as Record<string, unknown>)
: {};
for (const id in extensionURLs) {
window.eureka.registeredExtension[id] = {
url: projectJSON.extensionURLs[id],
env:
typeof projectJSON.extensionEnvs === 'object'
? projectJSON.extensionEnvs[id]
: 'sandboxed'
url: String(extensionURLs[id]),
env: String(extensionEnvs[id] ?? 'sandboxed')
};
}
for (const target of projectJSON.targets) {
for (const blockId in target.blocks) {
const block = target.blocks[blockId];
if (block.opcode === 'procedures_call' && 'mutation' in block) {
if (!block.mutation.proccode.trim().startsWith('[📎 Sideload] ')) {
continue;
}
const originalOpcode = block.mutation.proccode.trim().substring(14);
const extensionId = getExtensionIdForOpcode(originalOpcode);
if (!extensionId) {
warn(
`find a sideload block with an invalid id: ${originalOpcode}, ignored.`
);
continue;
}
if (!(extensionId in window.eureka.registeredExtension)) {
warn(
`find a sideload block with unregistered extension: ${extensionId}, ignored.`
);
continue;
if (projectJSON.targets instanceof Array) {
for (const target of projectJSON.targets) {
for (const blockId in target.blocks) {
const block = target.blocks[blockId];
if (block.opcode === 'procedures_call' && 'mutation' in block) {
if (!block.mutation.proccode.trim().startsWith('[📎 Sideload] ')) {
continue;
}
const originalOpcode = block.mutation.proccode.trim().substring(14);
const extensionId = getExtensionIdForOpcode(originalOpcode);
if (!extensionId) {
warn(
`find a sideload block with an invalid id: ${originalOpcode}, ignored.`
);
continue;
}
if (!(extensionId in window.eureka.registeredExtension)) {
warn(
`find a sideload block with unregistered extension: ${extensionId}, ignored.`
);
continue;
}
block.opcode = originalOpcode;
delete block.mutation;
}
block.opcode = originalOpcode;
delete block.mutation;
}
}
}
if ('sideloadMonitors' in projectJSON) {
if (
projectJSON.sideloadMonitors instanceof Array &&
projectJSON.monitors instanceof Array
) {
projectJSON.monitors.push(...projectJSON.sideloadMonitors);
delete projectJSON.sideloadMonitors;
}
}
// @ts-expect-error internal hack
return originalDrserializeFunc.call(this, projectJSON, ...args);
};

Expand Down Expand Up @@ -344,7 +366,10 @@ export function inject (vm: EurekaCompatibleVM) {
) {
for (const extensionId of extensions) {
if (
!vm.ccExtensionManager!.info.hasOwnProperty(extensionId) &&
!Object.prototype.hasOwnProperty.call(
vm.ccExtensionManager!.info,
extensionId
) &&
extensionId in window.eureka.registeredExtension
) {
vm.ccExtensionManager!.info[extensionId] = {
Expand All @@ -358,46 +383,58 @@ export function inject (vm: EurekaCompatibleVM) {
}

// Blockly stuffs
vm.once('workspaceUpdate', () => {
const blockly = (window.eureka.blockly = getBlocklyInstance(vm));
if (!blockly) {
let initalized = false;
getBlocklyInstance(vm).then((blockly) => {
if (!initalized) {
window.eureka.blockly = blockly;
initalized = true;
const originalAddCreateButton_ = blockly.Procedures.addCreateButton_;
blockly.Procedures.addCreateButton_ = function (
workspace: EurekaCompatibleWorkspace,
xmlList: HTMLElement[],
...args: unknown[]
) {
originalAddCreateButton_.call(this, workspace, xmlList, ...args);
injectToolbox(xmlList, workspace, format);
};
const workspace = blockly.getMainWorkspace();
workspace.getToolbox().refreshSelection();
workspace.toolboxRefreshEnabled_ = true;
}
});
setTimeout(() => {
if (!initalized) {
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;
}
initalized = true;
window.Blockly.getMainWorkspace().toolboxCategoryCallbacks_.PROCEDURE = function (
workspace: EurekaCompatibleWorkspace,
...args: unknown[]
) {
const xmlList = originalProcedureCallback.call(this, workspace, ...args) as HTMLElement[];
const xmlList = originalProcedureCallback.call(
this,
workspace,
...args
) as HTMLElement[];
injectToolbox(xmlList, workspace, format);
return xmlList;
};
const workspace = window.Blockly.getMainWorkspace();
workspace.getToolbox().refreshSelection();
workspace.toolboxRefreshEnabled_ = true;
return;
}

const originalAddCreateButton_ = blockly.Procedures.addCreateButton_;
blockly.Procedures.addCreateButton_ = function (
workspace: EurekaCompatibleWorkspace,
xmlList: HTMLElement[],
...args: unknown[]
) {
originalAddCreateButton_.call(this, workspace, xmlList, ...args);
injectToolbox(xmlList, workspace, format);
};
const workspace = blockly.getMainWorkspace();
workspace.getToolbox().refreshSelection();
workspace.toolboxRefreshEnabled_ = true;
});
}, 3000);
}

function injectToolbox (xmlList: HTMLElement[], workspace: EurekaCompatibleWorkspace, format: typeof formatMessage) {
function injectToolbox (
xmlList: HTMLElement[],
workspace: EurekaCompatibleWorkspace,
format: typeof formatMessage
) {
// Add separator and label
const sep = document.createElement('sep');
sep.setAttribute('gap', '36');
Expand Down
4 changes: 2 additions & 2 deletions src/loader/dispatch/central-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class _CentralDispatch extends SharedDispatch {
if (isRemote) {
throw new Error(`Cannot use 'callSync' on remote provider for service ${service}.`);
}
return provider[method].apply(provider, args);
return provider[method](...args);
}
throw new Error(`Provider not found for service: ${service}`);
}
Expand All @@ -55,7 +55,7 @@ class _CentralDispatch extends SharedDispatch {
* @param {object} provider - a local object which provides this service.
*/
setServiceSync (service: string, provider: any) {
if (this.services.hasOwnProperty(service)) {
if (Object.prototype.hasOwnProperty.call(this.services, service)) {
console.warn(`Central dispatch replacing existing service provider for ${service}`);
}
this.services[service] = provider;
Expand Down
4 changes: 1 addition & 3 deletions src/loader/dispatch/shared-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,7 @@ class SharedDispatch {
return obj.map((item) => this._purifyObject(item, visited, depth + 1));
}
const result: Record<string, unknown> = {};
for (const key in obj) {
// @ts-expect-error
const value = obj[key];
for (const [key, value] of Object.entries(obj)) {
result[key] = this._purifyObject(value, visited, depth + 1);
}
return result;
Expand Down
Loading

0 comments on commit 3fa1eab

Please sign in to comment.