refactor(client): allow TS worker to initialize itself and have the client check readiness (#57055)

This commit is contained in:
Oliver Eyton-Williams
2024-11-11 17:18:16 +01:00
committed by GitHub
parent e32c3d9a11
commit c7936b44b9
9 changed files with 44 additions and 32 deletions

View File

@@ -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),

View File

@@ -30,10 +30,10 @@ export function compileTypeScriptCode(code: string): Promise<string> {
});
}
export function initTypeScriptService(): Promise<boolean> {
export function checkTSServiceIsReady(): Promise<boolean> {
return awaitResponse({
worker: getTypeScriptWorker(),
message: { type: 'init' },
message: { type: 'check-is-ready' },
onMessage: (data, onSuccess) => {
if (data.type === 'ready') {
onSuccess(true);

View File

@@ -1,4 +1,4 @@
import { PyodideInterface } from 'pyodide';
import type { PyodideInterface } from 'pyodide';
export interface FrameDocument extends Document {
__initTestFrame: (e: InitTestFrameArg) => Promise<void>;

View File

@@ -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",

View File

@@ -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';

View File

@@ -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)
}
}
}

View File

@@ -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' });
}

View File

@@ -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:

View File

@@ -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')
},