mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
refactor: modularize typescript worker (#62668)
This commit is contained in:
committed by
GitHub
parent
e8f8b044cd
commit
df1c1a3f3f
@@ -0,0 +1,88 @@
|
|||||||
|
import type { VirtualTypeScriptEnvironment } from '@typescript/vfs';
|
||||||
|
import type { CompilerHost, CompilerOptions } from 'typescript';
|
||||||
|
|
||||||
|
import reactTypes from './react-types.json';
|
||||||
|
|
||||||
|
type TS = typeof import('typescript');
|
||||||
|
type TSVFS = typeof import('@typescript/vfs');
|
||||||
|
|
||||||
|
export class Compiler {
|
||||||
|
ts: TS;
|
||||||
|
tsvfs: TSVFS;
|
||||||
|
tsEnv?: VirtualTypeScriptEnvironment;
|
||||||
|
compilerHost?: CompilerHost;
|
||||||
|
constructor(
|
||||||
|
ts: typeof import('typescript'),
|
||||||
|
tsvfs: typeof import('@typescript/vfs')
|
||||||
|
) {
|
||||||
|
this.ts = ts;
|
||||||
|
this.tsvfs = tsvfs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setup() {
|
||||||
|
const ts = this.ts;
|
||||||
|
const tsvfs = this.tsvfs;
|
||||||
|
|
||||||
|
const compilerOptions: CompilerOptions = {
|
||||||
|
target: ts.ScriptTarget.ES2015,
|
||||||
|
module: ts.ModuleKind.Preserve, // Babel is handling module transformation, so TS should leave them alone.
|
||||||
|
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?
|
||||||
|
// from the docs: "Note: it's possible for this list to get out of
|
||||||
|
// 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.
|
||||||
|
};
|
||||||
|
const fsMap = await tsvfs.createDefaultMapFromCDN(
|
||||||
|
compilerOptions,
|
||||||
|
ts.version,
|
||||||
|
false, // TODO: cache this. It needs a store that's available to workers and implements https://github.com/microsoft/TypeScript-Website/blob/ac68b8b8e4a621113c4ee45c4051002fd55ede24/packages/typescript-vfs/src/index.ts#L11
|
||||||
|
ts
|
||||||
|
);
|
||||||
|
|
||||||
|
// This can be any path, but doing this means import React from 'react' works, if we ever need it.
|
||||||
|
const reactTypesPath = `/node_modules/@types/react/index.d.ts`;
|
||||||
|
|
||||||
|
// It may be necessary to get all the types (global.d.ts etc)
|
||||||
|
fsMap.set(reactTypesPath, reactTypes['react-18'] || '');
|
||||||
|
|
||||||
|
const system = tsvfs.createSystem(fsMap);
|
||||||
|
// TODO: if passed an invalid compiler options object (e.g. { module:
|
||||||
|
// ts.ModuleKind.CommonJS, moduleResolution: ts.ModuleResolutionKind.NodeNext
|
||||||
|
// }), this will throw. When we allow users to set compiler options, we should
|
||||||
|
// show them the diagnostics from this function.
|
||||||
|
this.tsEnv = tsvfs.createVirtualTypeScriptEnvironment(
|
||||||
|
system,
|
||||||
|
[reactTypesPath],
|
||||||
|
ts,
|
||||||
|
compilerOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
this.compilerHost = tsvfs.createVirtualCompilerHost(
|
||||||
|
system,
|
||||||
|
compilerOptions,
|
||||||
|
ts
|
||||||
|
).compilerHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
compile(code: string, fileName: string) {
|
||||||
|
if (!this.tsEnv || !this.compilerHost) {
|
||||||
|
throw Error('TypeScript environment not set up');
|
||||||
|
}
|
||||||
|
// TODO: If creating the file fresh each time is too slow, we can try checking
|
||||||
|
// if the file exists and updating it if it does.
|
||||||
|
this.tsEnv.createFile(fileName, code);
|
||||||
|
|
||||||
|
const program = this.tsEnv.languageService.getProgram()!;
|
||||||
|
|
||||||
|
const emitOutput = this.tsEnv.languageService.getEmitOutput(fileName);
|
||||||
|
const result = emitOutput.outputFiles[0].text;
|
||||||
|
|
||||||
|
const error = this.ts.formatDiagnostics(
|
||||||
|
this.ts.getPreEmitDiagnostics(program),
|
||||||
|
this.compilerHost
|
||||||
|
);
|
||||||
|
|
||||||
|
return { result, error };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
import { type VirtualTypeScriptEnvironment } from '@typescript/vfs';
|
import { Compiler } from './modules/typescript-compiler';
|
||||||
import type { CompilerOptions, CompilerHost } from 'typescript';
|
|
||||||
import reactTypes from './react-types.json';
|
|
||||||
|
|
||||||
// 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
|
||||||
// TS in this worker. The specific TS version is going to be determined by the
|
// TS in this worker. The specific TS version is going to be determined by the
|
||||||
// challenge (in general - it will be hardcoded in the MVP). So, the vfs types
|
// challenge (in general - it will be hardcoded in the MVP). So, the vfs types
|
||||||
// should be correct, but ts may not be.
|
// should be correct, but ts may not be.
|
||||||
declare const tsvfs: typeof import('@typescript/vfs');
|
declare const tsvfs: typeof import('@typescript/vfs');
|
||||||
declare const createDefaultMapFromCDN: typeof import('@typescript/vfs').createDefaultMapFromCDN;
|
|
||||||
declare const createVirtualCompilerHost: typeof import('@typescript/vfs').createVirtualCompilerHost;
|
|
||||||
declare const ts: typeof import('typescript');
|
declare const ts: typeof import('typescript');
|
||||||
|
|
||||||
const ctx: Worker & typeof globalThis = self as unknown as Worker &
|
const ctx: Worker & typeof globalThis = self as unknown as Worker &
|
||||||
@@ -43,13 +39,11 @@ interface CancelEvent extends MessageEvent {
|
|||||||
// Pin at the latest TS version available as cdnjs doesn't support version range.
|
// Pin at the latest TS version available as cdnjs doesn't support version range.
|
||||||
const TS_VERSION = '5.9.2';
|
const TS_VERSION = '5.9.2';
|
||||||
|
|
||||||
let tsEnv: VirtualTypeScriptEnvironment | null = null;
|
|
||||||
let compilerHost: CompilerHost | null = null;
|
|
||||||
let cachedVersion: string | null = null;
|
let cachedVersion: string | null = null;
|
||||||
|
|
||||||
// NOTE: vfs.globals must only be imported once, otherwise it will throw.
|
// NOTE: vfs.globals must only be imported once, otherwise it will throw.
|
||||||
importScripts(
|
importScripts(
|
||||||
'https://cdn.jsdelivr.net/npm/@typescript/vfs@1.6.0/dist/vfs.globals.js'
|
'https://cdnjs.cloudflare.com/ajax/libs/typescript-vfs/1.6.1/vfs.globals.js'
|
||||||
);
|
);
|
||||||
|
|
||||||
function importTS(version: string) {
|
function importTS(version: string) {
|
||||||
@@ -65,57 +59,6 @@ function importTS(version: string) {
|
|||||||
cachedVersion = version;
|
cachedVersion = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupTypeScript() {
|
|
||||||
importTS(TS_VERSION);
|
|
||||||
const compilerOptions: CompilerOptions = {
|
|
||||||
target: ts.ScriptTarget.ES2015,
|
|
||||||
module: ts.ModuleKind.Preserve, // Babel is handling module transformation, so TS should leave them alone.
|
|
||||||
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?
|
|
||||||
// from the docs: "Note: it's possible for this list to get out of
|
|
||||||
// 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.
|
|
||||||
};
|
|
||||||
const fsMap = await createDefaultMapFromCDN(
|
|
||||||
compilerOptions,
|
|
||||||
ts.version,
|
|
||||||
false, // TODO: cache this. It needs a store that's available to workers and implements https://github.com/microsoft/TypeScript-Website/blob/ac68b8b8e4a621113c4ee45c4051002fd55ede24/packages/typescript-vfs/src/index.ts#L11
|
|
||||||
ts
|
|
||||||
);
|
|
||||||
|
|
||||||
// This can be any path, but doing this means import React from 'react' works, if we ever need it.
|
|
||||||
const reactTypesPath = `/node_modules/@types/react/index.d.ts`;
|
|
||||||
|
|
||||||
// It may be necessary to get all the types (global.d.ts etc)
|
|
||||||
fsMap.set(reactTypesPath, reactTypes['react-18'] || '');
|
|
||||||
|
|
||||||
const system = tsvfs.createSystem(fsMap);
|
|
||||||
// TODO: if passed an invalid compiler options object (e.g. { module:
|
|
||||||
// ts.ModuleKind.CommonJS, moduleResolution: ts.ModuleResolutionKind.NodeNext
|
|
||||||
// }), this will throw. When we allow users to set compiler options, we should
|
|
||||||
// show them the diagnostics from this function.
|
|
||||||
const env = tsvfs.createVirtualTypeScriptEnvironment(
|
|
||||||
system,
|
|
||||||
[reactTypesPath],
|
|
||||||
ts,
|
|
||||||
compilerOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
compilerHost = createVirtualCompilerHost(
|
|
||||||
system,
|
|
||||||
compilerOptions,
|
|
||||||
ts
|
|
||||||
).compilerHost;
|
|
||||||
|
|
||||||
tsEnv = env;
|
|
||||||
|
|
||||||
// We freeze this to prevent learners from getting the worker into a
|
|
||||||
// weird state.
|
|
||||||
Object.freeze(self);
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.onmessage = (
|
ctx.onmessage = (
|
||||||
e: TSCompileEvent | CheckIsReadyRequestEvent | CancelEvent
|
e: TSCompileEvent | CheckIsReadyRequestEvent | CancelEvent
|
||||||
) => {
|
) => {
|
||||||
@@ -129,7 +72,10 @@ ctx.onmessage = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isTSSetup = setupTypeScript();
|
importTS(TS_VERSION);
|
||||||
|
|
||||||
|
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 }) {
|
||||||
@@ -137,7 +83,11 @@ function handleCancelRequest({ value }: { value: number }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleCheckIsReadyRequest(port: MessagePort) {
|
async function handleCheckIsReadyRequest(port: MessagePort) {
|
||||||
await isTSSetup;
|
await isSetup;
|
||||||
|
// We freeze this to prevent learners from getting the worker into a weird
|
||||||
|
// state.
|
||||||
|
Object.freeze(self);
|
||||||
|
|
||||||
port.postMessage({ type: 'ready' });
|
port.postMessage({ type: 'ready' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,25 +96,11 @@ function handleCompileRequest(data: TSCompileEvent['data'], port: MessagePort) {
|
|||||||
// permanently unable to interact with that file. The workaround is to create
|
// permanently unable to interact with that file. The workaround is to create
|
||||||
// a file with a single newline character.
|
// a file with a single newline character.
|
||||||
const code = (data.code || '').slice() || '\n';
|
const code = (data.code || '').slice() || '\n';
|
||||||
|
const { result, error } = compiler.compile(code, 'index.tsx');
|
||||||
// TODO: If creating the file fresh each time is too slow, we can try checking
|
|
||||||
// if the file exists and updating it if it does.
|
|
||||||
// TODO: make sure the .tsx extension doesn't cause issues with vanilla TS.
|
|
||||||
tsEnv?.createFile('/index.tsx', code);
|
|
||||||
|
|
||||||
const program = tsEnv!.languageService.getProgram()!;
|
|
||||||
|
|
||||||
const emitOutput = tsEnv!.languageService.getEmitOutput('index.tsx');
|
|
||||||
const compiled = emitOutput.outputFiles[0].text;
|
|
||||||
|
|
||||||
const message: TSCompiledMessage = {
|
const message: TSCompiledMessage = {
|
||||||
type: 'compiled',
|
type: 'compiled',
|
||||||
value: compiled,
|
value: result,
|
||||||
// TODO: stop forcing the non-null assertions here.
|
error: error
|
||||||
error: ts.formatDiagnostics(
|
|
||||||
ts.getPreEmitDiagnostics(program),
|
|
||||||
compilerHost!
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
port.postMessage(message);
|
port.postMessage(message);
|
||||||
|
|||||||
Reference in New Issue
Block a user