Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 fix: eureka is not working on player page #57

Merged
merged 1 commit into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { trap, injectVM, injectBlockly } from './injector/inject';
import { getVMInstance, getBlocklyInstance, injectVM, injectBlockly, initalizeEureka } from './injector/inject';
import { log } from './util/log';
const open = window.open;
// @ts-expect-error defined in webpack define plugin
log(`eureka-loader ${__EUREKA_VERSION__}`);
// Try injecting chibi into the page.
const [vm, blockly] = await trap(open);
// Initialize Eureka global object.
initalizeEureka();
// Try injecting Eureka into the page.
const vm = await getVMInstance();
if (vm) {
// Alright we got the virtual machine, start the injection.
window.eureka.vm = vm;
injectVM(vm);
injectBlockly(blockly);
getBlocklyInstance(vm).then(blockly => injectBlockly(blockly));
} else {
// This is not a Scratch page, stop injecting.
log(`Cannot find vm in this page, stop injecting.`);
Expand Down
106 changes: 65 additions & 41 deletions src/injector/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ interface EurekaCompatibleVM extends VM {
};
setLocale?: (locale: string, ...args: unknown[]) => unknown;
getLocale?: () => string;
_loadExtensions?: (extensionIDs: Set<string>, extensionURLs: Map<string, string>, ...args: unknown[]) => Promise<unknown>;
_loadExtensions?: (
extensionIDs: Set<string>,
extensionURLs: Map<string, string>,
...args: unknown[]
) => Promise<unknown>;
}

const MAX_LISTENING_MS = 30 * 1000;
Expand Down Expand Up @@ -63,7 +67,7 @@ function getExtensionIdForOpcode (opcode: string) {
* @param vm Virtual machine instance. For some reasons we cannot use VM here.
* @returns Blockly instance.
*/
async function getBlocklyInstance (vm: EurekaCompatibleVM): Promise<typeof Blockly | null> {
export async function getBlocklyInstance (vm: EurekaCompatibleVM): Promise<typeof Blockly | null> {
function getBlocklyInstanceInternal (): any | null {
// Hijack Function.prototype.apply to get React element instance.
function hijack (fn: (...args: unknown[]) => unknown) {
Expand Down Expand Up @@ -124,50 +128,54 @@ async function getBlocklyInstance (vm: EurekaCompatibleVM): Promise<typeof Block
}

/**
* Trap to get Virtual Machine instance.
* @param open window.open function (compatible with ccw).
* @return Callback promise. After that you could use window.eureka.vm to get the virtual machine.
* Initalize eureka global object.
*/
export function trap (open: typeof window.open): Promise<[EurekaCompatibleVM | null, typeof Blockly | null]> {
export function initalizeEureka () {
window.eureka = {
// @ts-expect-error defined in webpack define plugin
version: __EUREKA_VERSION__,
registeredExtension: {},
settings: settings,
openFrontend: openFrontend.bind(null, open)
openFrontend: openFrontend.bind(null, window.open)
};
}

/**
* Trap to get Virtual Machine instance.
* @return Callback promise. After that you could use window.eureka.vm to get the virtual machine.
*/
export async function getVMInstance (): Promise<EurekaCompatibleVM | null> {
log('Listening bind function...');
const oldBind = Function.prototype.bind;
// @ts-expect-error lazy to fix
return new Promise<EurekaCompatibleVM>((resolve, reject) => {
const timeoutId = setTimeout(() => {
log('Cannot find vm instance, stop listening.');
Function.prototype.bind = oldBind;
reject();
}, MAX_LISTENING_MS);

Function.prototype.bind = function (...args) {
if (Function.prototype.bind === oldBind) {
return oldBind.apply(this, args);
} else if (
args[0] &&
Object.prototype.hasOwnProperty.call(args[0], 'editingTarget') &&
Object.prototype.hasOwnProperty.call(args[0], 'runtime')
) {
log('VM detected!');
try {
const vm = await new Promise<EurekaCompatibleVM>((resolve, reject) => {
const timeoutId = setTimeout(() => {
log('Cannot find vm instance, stop listening.');
Function.prototype.bind = oldBind;
clearTimeout(timeoutId);
resolve(args[0]);
reject();
}, MAX_LISTENING_MS);

Function.prototype.bind = function (...args) {
if (Function.prototype.bind === oldBind) {
return oldBind.apply(this, args);
} else if (
args[0] &&
Object.prototype.hasOwnProperty.call(args[0], 'editingTarget') &&
Object.prototype.hasOwnProperty.call(args[0], 'runtime')
) {
log('VM detected!');
Function.prototype.bind = oldBind;
clearTimeout(timeoutId);
resolve(args[0]);
return oldBind.apply(this, args);
}
return oldBind.apply(this, args);
}
return oldBind.apply(this, args);
};
}).then(async (vm) => {
return [vm, await getBlocklyInstance(vm)];
}).catch(() => {
return [null, null];
});
};
});
return vm;
} catch {
return null;
}
}
function setupFormat (vm: EurekaCompatibleVM) {
const getLocale = vm.getLocale;
Expand Down Expand Up @@ -311,7 +319,10 @@ export function injectVM (vm: EurekaCompatibleVM) {

const originalDrserializeFunc = vm.deserializeProject;
vm.deserializeProject = function (projectJSON: Record<string, unknown>, ...args) {
if (typeof projectJSON.extensionURLs === 'object' || typeof projectJSON.sideloadExtensionURLs === 'object') {
if (
typeof projectJSON.extensionURLs === 'object' ||
typeof projectJSON.sideloadExtensionURLs === 'object'
) {
const extensionURLs: Record<string, unknown> =
typeof projectJSON.sideloadExtensionURLs === 'object'
? (projectJSON.sideloadExtensionURLs as Record<string, unknown>)
Expand All @@ -324,12 +335,18 @@ export function injectVM (vm: EurekaCompatibleVM) {
// Migrate from old eureka
if (projectJSON.extensionEnvs) {
log('Old eureka-ify project detected, migrating...');
extensionEnvs = projectJSON.sideloadExtensionEnvs = projectJSON.extensionEnvs as Record<string, unknown>;
extensionEnvs = projectJSON.sideloadExtensionEnvs =
projectJSON.extensionEnvs as Record<string, unknown>;
delete projectJSON.extensionEnvs;

for (const extensionId in (projectJSON.sideloadExtensionEnvs as Record<string, unknown>)) {
for (const extensionId in projectJSON.sideloadExtensionEnvs as Record<
string,
unknown
>) {
if (extensionId in (projectJSON.extensionURLs as Record<string, unknown>)) {
extensionURLs[extensionId] = (projectJSON.extensionURLs as Record<string, unknown>)[extensionId];
extensionURLs[extensionId] = (
projectJSON.extensionURLs as Record<string, unknown>
)[extensionId];
// @ts-expect-error lazy to fix types
delete projectJSON.extensionURLs[extensionId];
}
Expand Down Expand Up @@ -384,7 +401,11 @@ export function injectVM (vm: EurekaCompatibleVM) {
// Turbowarp-specific patch, skip security manager check
const originalTwLoadExtFunc = vm._loadExtensions;
if (typeof originalTwLoadExtFunc === 'function') {
vm._loadExtensions = function (extensionIDs: Set<string>, extensionURLs: Map<string, string>, ...args: unknown[]) {
vm._loadExtensions = function (
extensionIDs: Set<string>,
extensionURLs: Map<string, string>,
...args: unknown[]
) {
const sideloadExtensionPromises: Promise<void>[] = [];
for (const extensionId of extensionIDs) {
if (extensionId in window.eureka.registeredExtension) {
Expand All @@ -395,7 +416,10 @@ export function injectVM (vm: EurekaCompatibleVM) {
extensionIDs.delete(extensionId);
}
}
return Promise.all([originalTwLoadExtFunc.call(this, extensionIDs, extensionURLs, ...args), ...sideloadExtensionPromises]);
return Promise.all([
originalTwLoadExtFunc.call(this, extensionIDs, extensionURLs, ...args),
...sideloadExtensionPromises
]);
};
}

Expand Down Expand Up @@ -482,7 +506,7 @@ export function injectBlockly (blockly: any) {
} else {
warn('Cannot find real blockly instance, try alternative method...');
const originalProcedureCallback =
window.Blockly?.getMainWorkspace()?.toolboxCategoryCallbacks_?.PROCEDURE;
window.Blockly?.getMainWorkspace()?.toolboxCategoryCallbacks_?.PROCEDURE;
if (typeof originalProcedureCallback !== 'function') {
error('alternative method failed, stop injecting');
return;
Expand Down
Loading