feat: allow configuration of the typescript-compiler (#66241)

This commit is contained in:
Oliver Eyton-Williams
2026-03-05 05:18:18 +01:00
committed by GitHub
parent c94fe2d40e
commit 2972485a87
5 changed files with 27 additions and 18 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ vi.mock(
await compiler.setup({ useNodeModules: true }); await compiler.setup({ useNodeModules: true });
return { return {
...actual, ...actual,
checkTSServiceIsReady: () => Promise.resolve(true), setupTSCompiler: () => Promise.resolve(true),
compileTypeScriptCode: code => { compileTypeScriptCode: code => {
const { result, error } = compiler.compile(code, 'index.tsx'); const { result, error } = compiler.compile(code, 'index.tsx');
if (error) throw error; if (error) throw error;
@@ -20,7 +20,7 @@ import { version } from '@freecodecamp/browser-scripts/package.json';
import { WorkerExecutor } from './worker-executor'; import { WorkerExecutor } from './worker-executor';
import { import {
compileTypeScriptCode, compileTypeScriptCode,
checkTSServiceIsReady setupTSCompiler
} from './typescript-worker-handler'; } from './typescript-worker-handler';
const protectTimeout = 100; const protectTimeout = 100;
@@ -148,7 +148,7 @@ const getJSXModuleTranspiler = loopProtectOptions => async challengeFile => {
const getTSTranspiler = loopProtectOptions => async challengeFile => { const getTSTranspiler = loopProtectOptions => async challengeFile => {
await loadBabel(); await loadBabel();
await checkTSServiceIsReady(); await setupTSCompiler();
const babelOptions = getBabelOptions(presetsJS, loopProtectOptions); const babelOptions = getBabelOptions(presetsJS, loopProtectOptions);
return flow( return flow(
partial(transformHeadTailAndContents, compileTypeScriptCode), partial(transformHeadTailAndContents, compileTypeScriptCode),
@@ -159,7 +159,7 @@ const getTSTranspiler = loopProtectOptions => async challengeFile => {
const getTSXModuleTranspiler = loopProtectOptions => async challengeFile => { const getTSXModuleTranspiler = loopProtectOptions => async challengeFile => {
await loadBabel(); await loadBabel();
await loadPresetReact(); await loadPresetReact();
await checkTSServiceIsReady(); await setupTSCompiler();
const baseOptions = getBabelOptions(presetsJSX, loopProtectOptions); const baseOptions = getBabelOptions(presetsJSX, loopProtectOptions);
const babelOptions = { const babelOptions = {
...baseOptions, ...baseOptions,
@@ -31,10 +31,12 @@ export function compileTypeScriptCode(code: string): Promise<string> {
}); });
} }
export function checkTSServiceIsReady(): Promise<boolean> { export function setupTSCompiler(
compilerOptions?: Record<string, unknown>
): Promise<boolean> {
return awaitResponse({ return awaitResponse({
messenger: getTypeScriptWorker(), messenger: getTypeScriptWorker(),
message: { type: 'check-is-ready' }, message: { type: 'setup', ...(compilerOptions && { compilerOptions }) },
onMessage: (data, onSuccess) => { onMessage: (data, onSuccess) => {
if (data.type === 'ready') { if (data.type === 'ready') {
onSuccess(true); onSuccess(true);
@@ -16,10 +16,15 @@ export class Compiler {
this.tsvfs = tsvfs; this.tsvfs = tsvfs;
} }
async setup(opts?: { useNodeModules: boolean }) { async setup(opts?: { useNodeModules?: boolean; compilerOptions?: unknown }) {
const ts = this.ts; const ts = this.ts;
const tsvfs = this.tsvfs; const tsvfs = this.tsvfs;
const parsedOptions = ts.convertCompilerOptionsFromJson(
opts?.compilerOptions ?? {},
'/'
);
const compilerOptions: CompilerOptions = { const compilerOptions: CompilerOptions = {
target: ts.ScriptTarget.ES2024, target: ts.ScriptTarget.ES2024,
module: ts.ModuleKind.Preserve, // Babel is handling module transformation, so TS should leave them alone. 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 // sync with TypeScript over time. It was last synced with TypeScript
// 3.8.0-rc." // 3.8.0-rc."
jsx: ts.JsxEmit.Preserve, // Babel will handle JSX, 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 const fsMap = opts?.useNodeModules
@@ -1,3 +1,4 @@
import type { CompilerOptions } from 'typescript';
import { Compiler } from './modules/typescript-compiler'; import { Compiler } from './modules/typescript-compiler';
// Most of the ts types are only a guideline. This is because we're not bundling // Most of the ts types are only a guideline. This is because we're not bundling
@@ -23,9 +24,10 @@ interface TSCompiledMessage {
error: string; error: string;
} }
interface CheckIsReadyRequestEvent extends MessageEvent { interface SetupEvent extends MessageEvent {
data: { data: {
type: 'check-is-ready'; type: 'setup';
compilerOptions?: CompilerOptions;
}; };
} }
@@ -59,12 +61,10 @@ function importTS(version: string) {
cachedVersion = version; cachedVersion = version;
} }
ctx.onmessage = ( ctx.onmessage = (e: TSCompileEvent | SetupEvent | CancelEvent) => {
e: TSCompileEvent | CheckIsReadyRequestEvent | CancelEvent
) => {
const { data, ports } = e; const { data, ports } = e;
if (data.type === 'check-is-ready') { if (data.type === 'setup') {
void handleCheckIsReadyRequest(ports[0]); void handleSetupRequest(data, ports[0]);
} else if (data.type === 'cancel') { } else if (data.type === 'cancel') {
handleCancelRequest(data); handleCancelRequest(data);
} else { } else {
@@ -75,15 +75,16 @@ ctx.onmessage = (
importTS(TS_VERSION); importTS(TS_VERSION);
const compiler = new Compiler(ts, tsvfs); const compiler = new Compiler(ts, tsvfs);
const isSetup = compiler.setup();
// This lets the client know that there is nothing to cancel. // This lets the client know that there is nothing to cancel.
function handleCancelRequest({ value }: { value: number }) { function handleCancelRequest({ value }: { value: number }) {
postMessage({ type: 'is-alive', text: value }); postMessage({ type: 'is-alive', text: value });
} }
async function handleCheckIsReadyRequest(port: MessagePort) { async function handleSetupRequest(data: SetupEvent['data'], port: MessagePort) {
await isSetup; await compiler.setup({
compilerOptions: data.compilerOptions
});
// We freeze this to prevent learners from getting the worker into a weird // We freeze this to prevent learners from getting the worker into a weird
// state. // state.
Object.freeze(self); Object.freeze(self);