diff --git a/client/src/templates/Challenges/rechallenge/transformers.js b/client/src/templates/Challenges/rechallenge/transformers.js index 18a18d94d2e..62c7ede15a8 100644 --- a/client/src/templates/Challenges/rechallenge/transformers.js +++ b/client/src/templates/Challenges/rechallenge/transformers.js @@ -20,7 +20,7 @@ import { import { WorkerExecutor } from '../utils/worker-executor'; import { compileTypeScriptCode, - initTypeScriptService + checkTSServiceIsReady } from '../utils/typescript-worker-handler'; const { filename: sassCompile } = sassData; @@ -146,7 +146,7 @@ const babelTransformer = loopProtectOptions => { testTypeScript, async challengeFile => { await loadBabel(); - await initTypeScriptService(); + await checkTSServiceIsReady(); const babelOptions = getBabelOptions(presetsJS, loopProtectOptions); return flow( partial(transformHeadTailAndContents, compileTypeScriptCode), diff --git a/client/src/templates/Challenges/utils/typescript-worker-handler.ts b/client/src/templates/Challenges/utils/typescript-worker-handler.ts index 81188a3299d..3bd3223ea4d 100644 --- a/client/src/templates/Challenges/utils/typescript-worker-handler.ts +++ b/client/src/templates/Challenges/utils/typescript-worker-handler.ts @@ -30,10 +30,10 @@ export function compileTypeScriptCode(code: string): Promise { }); } -export function initTypeScriptService(): Promise { +export function checkTSServiceIsReady(): Promise { return awaitResponse({ worker: getTypeScriptWorker(), - message: { type: 'init' }, + message: { type: 'check-is-ready' }, onMessage: (data, onSuccess) => { if (data.type === 'ready') { onSuccess(true); diff --git a/tools/client-plugins/browser-scripts/index.d.ts b/tools/client-plugins/browser-scripts/index.d.ts index e76967c006a..4367d1b3561 100644 --- a/tools/client-plugins/browser-scripts/index.d.ts +++ b/tools/client-plugins/browser-scripts/index.d.ts @@ -1,4 +1,4 @@ -import { PyodideInterface } from 'pyodide'; +import type { PyodideInterface } from 'pyodide'; export interface FrameDocument extends Document { __initTestFrame: (e: InitTestFrameArg) => Promise; diff --git a/tools/client-plugins/browser-scripts/package.json b/tools/client-plugins/browser-scripts/package.json index 0d6d2a87feb..f3bb8a66941 100644 --- a/tools/client-plugins/browser-scripts/package.json +++ b/tools/client-plugins/browser-scripts/package.json @@ -20,8 +20,9 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build": "NODE_OPTIONS=\"--max-old-space-size=7168\" webpack -c webpack.config.js" + "build": "NODE_OPTIONS=\"--max-old-space-size=7168\" webpack -c webpack.config.cjs" }, + "type": "module", "keywords": [], "devDependencies": { "@babel/plugin-syntax-dynamic-import": "7.8.3", diff --git a/tools/client-plugins/browser-scripts/python-worker.ts b/tools/client-plugins/browser-scripts/python-worker.ts index fdaea902be3..69840437f19 100644 --- a/tools/client-plugins/browser-scripts/python-worker.ts +++ b/tools/client-plugins/browser-scripts/python-worker.ts @@ -1,5 +1,10 @@ // We have to specify pyodide.js because we need to import that file (not .mjs) -// and 'import' defaults to .mjs +// and 'import' defaults to .mjs. + +// This is to do with how webpack handles node fallbacks - it uses the node +// resolution algorithm to find the file, but that requires the full file name. +// We can't add the extension, because it's in a bundle we're importing. However +// we can import the .js file and then the strictness does not apply. import { loadPyodide, type PyodideInterface } from 'pyodide/pyodide.js'; import pkg from 'pyodide/package.json'; import type { PyProxy, PythonError } from 'pyodide/ffi'; diff --git a/tools/client-plugins/browser-scripts/tsconfig.json b/tools/client-plugins/browser-scripts/tsconfig.json index af99915a71e..b0c33770fef 100644 --- a/tools/client-plugins/browser-scripts/tsconfig.json +++ b/tools/client-plugins/browser-scripts/tsconfig.json @@ -1,13 +1,17 @@ { "compilerOptions": { "target": "es2022", - "module": "CommonJS", + "module": "ESNext", + "moduleResolution": "Bundler", "lib": ["WebWorker", "DOM"], "allowJs": true, "strict": true, "forceConsistentCasingInFileNames": true, "esModuleInterop": true, "resolveJsonModule": true, - "noEmit": true + "noEmit": true, + "paths": { + "pyodide/ffi": ["./node_modules/pyodide/ffi.d.ts"] // Remove after updating pyodide (later versions correctly export ffi) + } } } diff --git a/tools/client-plugins/browser-scripts/typescript-worker.ts b/tools/client-plugins/browser-scripts/typescript-worker.ts index d34ca7f01d3..ebfb1adc72b 100644 --- a/tools/client-plugins/browser-scripts/typescript-worker.ts +++ b/tools/client-plugins/browser-scripts/typescript-worker.ts @@ -13,9 +13,6 @@ declare const ts: typeof import('typescript'); const ctx: Worker & typeof globalThis = self as unknown as Worker & typeof globalThis; -let tsEnv: VirtualTypeScriptEnvironment | null = null; -let compilerHost: CompilerHost | null = null; - interface TSCompileEvent extends MessageEvent { data: { type: 'compile'; @@ -29,9 +26,9 @@ interface TSCompiledMessage { error: string; } -interface InitRequestEvent extends MessageEvent { +interface CheckIsReadyRequestEvent extends MessageEvent { data: { - type: 'init'; + type: 'check-is-ready'; }; } @@ -42,15 +39,23 @@ interface CancelEvent extends MessageEvent { }; } +const TS_VERSION = '5'; // hardcoding for now, in the future this may be dynamic + +let tsEnv: VirtualTypeScriptEnvironment | null = null; +let compilerHost: CompilerHost | null = null; let cachedVersion: string | null = null; -async function setupTypeScript(version: string) { - // TODO: make sure no racing happens if multiple inits arrive at once. - if (cachedVersion == version) return tsEnv; +// NOTE: vfs.globals must only be imported once, otherwise it will throw. +importScripts('https://unpkg.com/@typescript/vfs@1.6.0/dist/vfs.globals.js'); +function importTS(version: string) { + if (cachedVersion == version) return; importScripts('https://unpkg.com/typescript@' + version); - importScripts('https://unpkg.com/@typescript/vfs@1.6.0/dist/vfs.globals.js'); + cachedVersion = version; +} +async function setupTypeScript() { + importTS(TS_VERSION); const compilerOptions: CompilerOptions = { target: ts.ScriptTarget.ES2015, skipLibCheck: true // TODO: look into why this is needed. Are we doing something wrong? Could it be that it's not "synced" with this TS version? @@ -87,20 +92,15 @@ async function setupTypeScript(version: string) { // We freeze this to prevent learners from getting the worker into a // weird state. Object.freeze(self); - - cachedVersion = version; return env; } -// TODO: figure out how to start setting up TS in the background, but allow the -// client to wait for it to be ready. Currently the waiting works, but the setup -// is done on demand. -// void setupTypeScript('5'); - -ctx.onmessage = (e: TSCompileEvent | InitRequestEvent | CancelEvent) => { +ctx.onmessage = ( + e: TSCompileEvent | CheckIsReadyRequestEvent | CancelEvent +) => { const { data, ports } = e; - if (data.type === 'init') { - void handleInitRequest(ports[0]); + if (data.type === 'check-is-ready') { + void handleCheckIsReadyRequest(ports[0]); } else if (data.type === 'cancel') { handleCancelRequest(data); } else { @@ -108,13 +108,15 @@ ctx.onmessage = (e: TSCompileEvent | InitRequestEvent | CancelEvent) => { } }; +const isTSSetup = setupTypeScript(); + // This lets the client know that there is nothing to cancel. function handleCancelRequest({ value }: { value: number }) { postMessage({ type: 'is-alive', text: value }); } -async function handleInitRequest(port: MessagePort) { - await setupTypeScript('5'); +async function handleCheckIsReadyRequest(port: MessagePort) { + await isTSSetup; port.postMessage({ type: 'ready' }); } diff --git a/tools/client-plugins/browser-scripts/utils/format.js b/tools/client-plugins/browser-scripts/utils/format.js index f9d3d137b19..fd73f31d986 100644 --- a/tools/client-plugins/browser-scripts/utils/format.js +++ b/tools/client-plugins/browser-scripts/utils/format.js @@ -1,7 +1,7 @@ // TODO: this is a straight up copy of the format function from the client. // Figure out a way to share it. -import { inspect } from 'util/util'; +import { inspect } from 'util/util.js'; export function format(x) { // we're trying to mimic console.log, so we avoid wrapping strings in quotes: diff --git a/tools/client-plugins/browser-scripts/webpack.config.js b/tools/client-plugins/browser-scripts/webpack.config.cjs similarity index 98% rename from tools/client-plugins/browser-scripts/webpack.config.js rename to tools/client-plugins/browser-scripts/webpack.config.cjs index 7e67e1c1312..21b6a085446 100644 --- a/tools/client-plugins/browser-scripts/webpack.config.js +++ b/tools/client-plugins/browser-scripts/webpack.config.cjs @@ -84,7 +84,7 @@ module.exports = (env = {}) => { resolve: { fallback: { buffer: require.resolve('buffer'), - util: false, + util: require.resolve('util'), stream: false, process: require.resolve('process/browser.js') },