From 2972485a8774d636abb60866e683516b535f3127 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Thu, 5 Mar 2026 05:18:18 +0100 Subject: [PATCH] feat: allow configuration of the typescript-compiler (#66241) --- curriculum/src/test/test-challenges.js | 2 +- .../challenge-builder/src/transformers.js | 6 +++--- .../src/typescript-worker-handler.ts | 6 ++++-- .../modules/typescript-compiler.ts | 10 +++++++-- .../browser-scripts/typescript-worker.ts | 21 ++++++++++--------- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/curriculum/src/test/test-challenges.js b/curriculum/src/test/test-challenges.js index 504e4917ae5..8ebf5b4b3cb 100644 --- a/curriculum/src/test/test-challenges.js +++ b/curriculum/src/test/test-challenges.js @@ -41,7 +41,7 @@ vi.mock( await compiler.setup({ useNodeModules: true }); return { ...actual, - checkTSServiceIsReady: () => Promise.resolve(true), + setupTSCompiler: () => Promise.resolve(true), compileTypeScriptCode: code => { const { result, error } = compiler.compile(code, 'index.tsx'); if (error) throw error; diff --git a/packages/challenge-builder/src/transformers.js b/packages/challenge-builder/src/transformers.js index db0a6b37337..034bff8db90 100644 --- a/packages/challenge-builder/src/transformers.js +++ b/packages/challenge-builder/src/transformers.js @@ -20,7 +20,7 @@ import { version } from '@freecodecamp/browser-scripts/package.json'; import { WorkerExecutor } from './worker-executor'; import { compileTypeScriptCode, - checkTSServiceIsReady + setupTSCompiler } from './typescript-worker-handler'; const protectTimeout = 100; @@ -148,7 +148,7 @@ const getJSXModuleTranspiler = loopProtectOptions => async challengeFile => { const getTSTranspiler = loopProtectOptions => async challengeFile => { await loadBabel(); - await checkTSServiceIsReady(); + await setupTSCompiler(); const babelOptions = getBabelOptions(presetsJS, loopProtectOptions); return flow( partial(transformHeadTailAndContents, compileTypeScriptCode), @@ -159,7 +159,7 @@ const getTSTranspiler = loopProtectOptions => async challengeFile => { const getTSXModuleTranspiler = loopProtectOptions => async challengeFile => { await loadBabel(); await loadPresetReact(); - await checkTSServiceIsReady(); + await setupTSCompiler(); const baseOptions = getBabelOptions(presetsJSX, loopProtectOptions); const babelOptions = { ...baseOptions, diff --git a/packages/challenge-builder/src/typescript-worker-handler.ts b/packages/challenge-builder/src/typescript-worker-handler.ts index da15802dbdc..a255a085082 100644 --- a/packages/challenge-builder/src/typescript-worker-handler.ts +++ b/packages/challenge-builder/src/typescript-worker-handler.ts @@ -31,10 +31,12 @@ export function compileTypeScriptCode(code: string): Promise { }); } -export function checkTSServiceIsReady(): Promise { +export function setupTSCompiler( + compilerOptions?: Record +): Promise { return awaitResponse({ messenger: getTypeScriptWorker(), - message: { type: 'check-is-ready' }, + message: { type: 'setup', ...(compilerOptions && { compilerOptions }) }, onMessage: (data, onSuccess) => { if (data.type === 'ready') { onSuccess(true); diff --git a/tools/client-plugins/browser-scripts/modules/typescript-compiler.ts b/tools/client-plugins/browser-scripts/modules/typescript-compiler.ts index 801da2b003e..1f457295667 100644 --- a/tools/client-plugins/browser-scripts/modules/typescript-compiler.ts +++ b/tools/client-plugins/browser-scripts/modules/typescript-compiler.ts @@ -16,10 +16,15 @@ export class Compiler { this.tsvfs = tsvfs; } - async setup(opts?: { useNodeModules: boolean }) { + async setup(opts?: { useNodeModules?: boolean; compilerOptions?: unknown }) { const ts = this.ts; const tsvfs = this.tsvfs; + const parsedOptions = ts.convertCompilerOptionsFromJson( + opts?.compilerOptions ?? {}, + '/' + ); + const compilerOptions: CompilerOptions = { target: ts.ScriptTarget.ES2024, module: ts.ModuleKind.Preserve, // Babel is handling module transformation, so TS should leave them alone. @@ -28,7 +33,8 @@ export class Compiler { // sync with TypeScript over time. It was last synced with TypeScript // 3.8.0-rc." jsx: ts.JsxEmit.Preserve, // Babel will handle JSX, - allowUmdGlobalAccess: true // Necessary because React is loaded via a UMD script. + allowUmdGlobalAccess: true, // Necessary because React is loaded via a UMD script. + ...parsedOptions.options }; const fsMap = opts?.useNodeModules diff --git a/tools/client-plugins/browser-scripts/typescript-worker.ts b/tools/client-plugins/browser-scripts/typescript-worker.ts index 255d116c306..bb5011bdf8f 100644 --- a/tools/client-plugins/browser-scripts/typescript-worker.ts +++ b/tools/client-plugins/browser-scripts/typescript-worker.ts @@ -1,3 +1,4 @@ +import type { CompilerOptions } from 'typescript'; import { Compiler } from './modules/typescript-compiler'; // Most of the ts types are only a guideline. This is because we're not bundling @@ -23,9 +24,10 @@ interface TSCompiledMessage { error: string; } -interface CheckIsReadyRequestEvent extends MessageEvent { +interface SetupEvent extends MessageEvent { data: { - type: 'check-is-ready'; + type: 'setup'; + compilerOptions?: CompilerOptions; }; } @@ -59,12 +61,10 @@ function importTS(version: string) { cachedVersion = version; } -ctx.onmessage = ( - e: TSCompileEvent | CheckIsReadyRequestEvent | CancelEvent -) => { +ctx.onmessage = (e: TSCompileEvent | SetupEvent | CancelEvent) => { const { data, ports } = e; - if (data.type === 'check-is-ready') { - void handleCheckIsReadyRequest(ports[0]); + if (data.type === 'setup') { + void handleSetupRequest(data, ports[0]); } else if (data.type === 'cancel') { handleCancelRequest(data); } else { @@ -75,15 +75,16 @@ ctx.onmessage = ( importTS(TS_VERSION); const compiler = new Compiler(ts, tsvfs); -const isSetup = compiler.setup(); // This lets the client know that there is nothing to cancel. function handleCancelRequest({ value }: { value: number }) { postMessage({ type: 'is-alive', text: value }); } -async function handleCheckIsReadyRequest(port: MessagePort) { - await isSetup; +async function handleSetupRequest(data: SetupEvent['data'], port: MessagePort) { + await compiler.setup({ + compilerOptions: data.compilerOptions + }); // We freeze this to prevent learners from getting the worker into a weird // state. Object.freeze(self);