import chai from 'chai'; import { toString as __toString } from 'lodash-es'; import * as helpers from '@freecodecamp/curriculum-helpers'; import { format as __format } from './utils/format'; const ctx: Worker & typeof globalThis = self as unknown as Worker & typeof globalThis; const __utils = (() => { const MAX_LOGS_SIZE = 64 * 1024; let logs: string[] = []; function flushLogs() { if (logs.length) { ctx.postMessage({ type: 'LOG', data: logs.join('\n') }); logs = []; } } function pushLogs(logs: string[], args: string[]) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return logs.push(args.map(arg => __format(arg)).join(' ')); if (logs.join('\n').length > MAX_LOGS_SIZE) { flushLogs(); } } const oldLog = ctx.console.log.bind(ctx.console); function proxyLog(...args: string[]) { pushLogs(logs, args); return oldLog(...args); } const oldInfo = ctx.console.info.bind(ctx.console); function proxyInfo(...args: string[]) { pushLogs(logs, args); return oldInfo(...args); } const oldWarn = ctx.console.warn.bind(ctx.console); function proxyWarn(...args: string[]) { pushLogs(logs, args); return oldWarn(...args); } const oldError = ctx.console.error.bind(ctx.console); function proxyError(...args: string[]) { pushLogs(logs, args); return oldError(...args); } function log(...msgs: Error[]) { if (msgs && msgs[0] && !(msgs[0] instanceof chai.AssertionError)) { // discards the stack trace via toString as it only useful to debug the // site, not a specific challenge. console.log(...msgs.map(msg => msg.toString())); } } const toggleProxyLogger = (on: unknown) => { ctx.console.log = on ? proxyLog : oldLog; ctx.console.info = on ? proxyInfo : oldInfo; ctx.console.warn = on ? proxyWarn : oldWarn; ctx.console.error = on ? proxyError : oldError; }; return { log, toggleProxyLogger, flushLogs }; })(); // We freeze these two to prevent learners from getting the tester into a weird // state. Object.freeze(self); Object.freeze(__utils); interface TestEvaluatorEvent extends MessageEvent { data: { code: { contents: string; editableContents: string; original: { [id: string]: string }; }; removeComments: boolean; firstTest: unknown; testString: string; build: string; sources: { [fileName: string]: unknown; }; }; } /* Run the test if there is one. If not just evaluate the user code */ ctx.onmessage = async (e: TestEvaluatorEvent) => { /* eslint-disable @typescript-eslint/no-unused-vars */ let code = (e.data?.code?.contents || '').slice(); code = e.data?.removeComments ? helpers.removeJSComments(code) : code; let editableContents = (e.data?.code?.editableContents || '').slice(); editableContents = e.data?.removeComments ? helpers.removeJSComments(editableContents) : editableContents; const assert = chai.assert; const __helpers = helpers; // Similarly to self and __utils, if the learner tries to modify these, weird // behavior may result. Freezing them means they get an error instead. Object.freeze(assert); Object.freeze(__helpers); // Fake Deep Equal dependency const DeepEqual = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b); // Build errors should be reported, but only once: __utils.toggleProxyLogger(e.data.firstTest); /* eslint-enable @typescript-eslint/no-unused-vars */ try { let testResult; // This can be reassigned by the eval inside the try block, so it should be declared as a let // eslint-disable-next-line prefer-const let __userCodeWasExecuted = false; /* eslint-disable no-eval */ try { // Logging is proxyed after the build to catch console.log messages // generated during testing. testResult = (await eval(`${ e.data?.removeComments ? helpers.removeJSComments(e.data.build) : e.data.build } __utils.flushLogs(); __userCodeWasExecuted = true; __utils.toggleProxyLogger(true); ${e.data.testString}`)) as unknown; } catch (err) { if (__userCodeWasExecuted) { // rethrow error, since test failed. throw err; } // log build errors unless they're related to import/export/require (there // are challenges that use them and they should not trigger warnings) if ( (err as Error).name !== 'ReferenceError' || ((err as Error).message !== 'require is not defined' && (err as Error).message !== 'exports is not defined') ) { __utils.log(err as Error); } // the tests may not require working code, so they are evaluated even if // the user code does not get executed. testResult = eval(e.data.testString) as unknown; } /* eslint-enable no-eval */ if (typeof testResult === 'function') { await testResult((fileName: string) => __toString(e.data.sources[fileName]) ); } __utils.flushLogs(); ctx.postMessage({ pass: true }); } catch (err) { // Errors from testing go to the browser console only. __utils.toggleProxyLogger(false); // Report execution errors in case user code has errors that are only // uncovered during testing. __utils.log(err as Error); // Now that all logs have been created we can flush them. __utils.flushLogs(); ctx.postMessage({ err: { message: (err as Error).message, stack: (err as Error).stack } }); } }; ctx.postMessage({ type: 'contentLoaded' });