chore: import upstream snapshot with attribution
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1 @@
|
||||
dist
|
||||
@@ -0,0 +1,4 @@
|
||||
/* eslint-disable filenames-simple/naming-convention */
|
||||
import { createLintStagedConfig } from '@freecodecamp/eslint-config/lintstaged';
|
||||
|
||||
export default createLintStagedConfig(import.meta.dirname);
|
||||
@@ -0,0 +1,27 @@
|
||||
import { cpSync, mkdirSync, rmSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { version as workerVersion } from '@freecodecamp/browser-scripts/package.json';
|
||||
import { version as helperVersion } from '@freecodecamp/curriculum-helpers/package.json';
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
const distDir = resolve(__dirname, 'dist');
|
||||
|
||||
const destJsDir = resolve(distDir, './js');
|
||||
|
||||
rmSync(distDir, { recursive: true, force: true });
|
||||
mkdirSync(destJsDir, { recursive: true });
|
||||
|
||||
cpSync(
|
||||
resolve(__dirname, './node_modules/sass.js/dist/sass.sync.js'),
|
||||
resolve(destJsDir, 'workers', workerVersion, 'sass.sync.js')
|
||||
);
|
||||
cpSync(
|
||||
resolve(
|
||||
__dirname,
|
||||
'./node_modules/@freecodecamp/curriculum-helpers/dist/test-runner'
|
||||
),
|
||||
resolve(destJsDir, `test-runner/${helperVersion}/`),
|
||||
{ recursive: true }
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
import { configTypeChecked } from '@freecodecamp/eslint-config/base';
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
...configTypeChecked,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node // TODO: migrate to ESM and remove globals
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import type { PyodideInterface } from 'pyodide';
|
||||
|
||||
export interface FrameDocument extends Document {
|
||||
__initTestFrame: (e: InitTestFrameArg) => Promise<void>;
|
||||
__runTest: (
|
||||
testString: string
|
||||
) => Promise<
|
||||
{ pass: boolean } | { err: { message: string; stack?: string } }
|
||||
>;
|
||||
}
|
||||
|
||||
export interface PythonDocument extends FrameDocument {
|
||||
__initPythonFrame: () => Promise<void>;
|
||||
__runPython: (code: string) => Promise<PyodideInterface>;
|
||||
}
|
||||
|
||||
export interface InitTestFrameArg {
|
||||
code: {
|
||||
contents?: string;
|
||||
editableContents?: string;
|
||||
};
|
||||
loadEnzyme?: () => void;
|
||||
}
|
||||
|
||||
export type FrameWindow = Window &
|
||||
typeof globalThis & {
|
||||
$: typeof $;
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,111 @@
|
||||
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: TS, tsvfs: TSVFS) {
|
||||
this.ts = ts;
|
||||
this.tsvfs = tsvfs;
|
||||
}
|
||||
|
||||
async setup(opts?: { useNodeModules?: boolean; tsconfig?: string }) {
|
||||
const ts = this.ts;
|
||||
const tsvfs = this.tsvfs;
|
||||
|
||||
// This just parses the JSON, it doesn't do any validation.
|
||||
const parsedOptions = opts?.tsconfig
|
||||
? (ts.parseConfigFileTextToJson('', opts.tsconfig).config as {
|
||||
compilerOptions?: unknown;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// For now we're only interested in the compilerOptions, so that's all we're
|
||||
// extracting and validating. For everything else, we could
|
||||
// parseJsonConfigFileContent and create a host using createSystem and
|
||||
// fsMap, but that needs compilerOptions... This is a bit of a chicken and
|
||||
// egg problem, which we don't need to solve yet.
|
||||
const validatedOptions = ts.convertCompilerOptionsFromJson(
|
||||
parsedOptions?.compilerOptions ?? {},
|
||||
'./'
|
||||
);
|
||||
|
||||
const compilerOptions: CompilerOptions = {
|
||||
target: ts.ScriptTarget.ES2024,
|
||||
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.
|
||||
...validatedOptions.options
|
||||
};
|
||||
|
||||
const fsMap = opts?.useNodeModules
|
||||
? tsvfs.createDefaultMapFromNodeModules(compilerOptions, ts)
|
||||
: 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(rawCode: string, fileName: string) {
|
||||
if (!this.tsEnv || !this.compilerHost) {
|
||||
throw Error('TypeScript environment not set up');
|
||||
}
|
||||
// If we try to update or create an empty file, the environment will become
|
||||
// permanently unable to interact with that file. The workaround is to create
|
||||
// a file with a single newline character.
|
||||
const code = rawCode || '\n';
|
||||
// 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 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@freecodecamp/browser-scripts",
|
||||
"version": "1.0.1",
|
||||
"description": "The freeCodeCamp.org open-source codebase and curriculum",
|
||||
"license": "BSD-3-Clause",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
"pnpm": ">=10"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": "./index.d.ts",
|
||||
"./ts-compiler": "./modules/typescript-compiler.ts",
|
||||
"./test-runner": "./test-runner.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
|
||||
},
|
||||
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"lint": "eslint --max-warnings 0",
|
||||
"build": "pnpm copy-scripts && pnpm bundle",
|
||||
"bundle": "NODE_OPTIONS=\"--max-old-space-size=7168\" webpack -c webpack.config.cjs --env production",
|
||||
"copy-scripts": "tsx ./copy-scripts.ts"
|
||||
},
|
||||
"type": "module",
|
||||
"keywords": [],
|
||||
"devDependencies": {
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/plugin-transform-runtime": "7.23.7",
|
||||
"@babel/preset-env": "7.23.7",
|
||||
"@babel/preset-typescript": "7.23.3",
|
||||
"@freecodecamp/eslint-config": "workspace:*",
|
||||
"@typescript/vfs": "1.6.4",
|
||||
"babel-loader": "8.4.1",
|
||||
"eslint": "^9.39.1",
|
||||
"process": "0.11.10",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "5.9.3",
|
||||
"util": "0.12.5",
|
||||
"webpack": "5.106.2",
|
||||
"webpack-cli": "4.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@freecodecamp/curriculum-helpers": "^9.0.1",
|
||||
"pyodide": "^0.23.3",
|
||||
"sass.js": "0.11.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
// We have to specify pyodide.js because we need to import that file (not .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';
|
||||
import { formatException } from '@freecodecamp/curriculum-helpers';
|
||||
|
||||
const ctx: Worker & typeof globalThis = self as unknown as Worker &
|
||||
typeof globalThis;
|
||||
|
||||
let pyodide: PyodideInterface | null = null;
|
||||
|
||||
interface PythonRunEvent extends MessageEvent {
|
||||
data: {
|
||||
type: 'run';
|
||||
code: {
|
||||
contents: string;
|
||||
editableContents: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface ListenRequestEvent extends MessageEvent {
|
||||
data: {
|
||||
type: 'listen';
|
||||
};
|
||||
}
|
||||
|
||||
interface CancelEvent extends MessageEvent {
|
||||
data: {
|
||||
type: 'cancel';
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Since messages are buffered, it needs to be possible to discard 'run'
|
||||
// messages. Otherwise messages could build up while the worker is busy (for
|
||||
// example, while loading pyodide) and the work would try to process them in
|
||||
// sequence. Instead, it will ignore messages until it receives a 'listen'
|
||||
// message and will inform the client every time it starts ignoring messages.
|
||||
let ignoreRunMessages = true;
|
||||
|
||||
async function setupPyodide() {
|
||||
if (pyodide) return pyodide;
|
||||
|
||||
pyodide = await loadPyodide({
|
||||
// TODO: host this ourselves
|
||||
indexURL: `https://cdn.jsdelivr.net/pyodide/v${pkg.version}/full/`
|
||||
});
|
||||
|
||||
// We freeze this to prevent learners from getting the worker into a
|
||||
// weird state. NOTE: this has to come after pyodide is loaded, because
|
||||
// pyodide modifies self while loading.
|
||||
Object.freeze(self);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
pyodide.FS.writeFile('/home/pyodide/format_exception.py', formatException, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
|
||||
ignoreRunMessages = true;
|
||||
postMessage({ type: 'stopped' });
|
||||
}
|
||||
|
||||
function resetPyodide() {
|
||||
if (pyodide) pyodide = null;
|
||||
void setupPyodide();
|
||||
}
|
||||
|
||||
void setupPyodide();
|
||||
|
||||
function initRunPython() {
|
||||
if (!pyodide) throw new Error('pyodide not loaded');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const str = pyodide.globals.get('str') as (x: unknown) => string;
|
||||
|
||||
function print(...args: unknown[]) {
|
||||
const text = args.map(x => str(x)).join(' ');
|
||||
postMessage({ type: 'print', text });
|
||||
}
|
||||
|
||||
function input(text: string) {
|
||||
// TODO: send unique ids to the main thread and the service worker, so we
|
||||
// can have multiple concurrent input requests.
|
||||
postMessage({ type: 'input', text });
|
||||
const request = new XMLHttpRequest();
|
||||
request.open('POST', '/python/intercept-input/', false);
|
||||
request.send(null);
|
||||
|
||||
// We want to raise a KeyboardInterrupt if the user cancels. To do that,
|
||||
// this function returns a JS object with the 'type' property set to
|
||||
// 'cancel'. Then the python code can actually raise the exception.
|
||||
return JSON.parse(request.responseText) as {
|
||||
type: 'msg' | 'cancel';
|
||||
value?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function __interruptExecution() {
|
||||
postMessage({ type: 'reset' });
|
||||
}
|
||||
|
||||
// I tried setting jsglobals here, to provide 'input' and 'print' to python,
|
||||
// without having to modify the global window object. However, it didn't work
|
||||
// because pyodide needs access to that object. Instead, I used
|
||||
// registerJsModule when setting up runPython.
|
||||
|
||||
// Make print available to python
|
||||
pyodide.registerJsModule('jscustom', {
|
||||
print,
|
||||
input,
|
||||
__interruptExecution
|
||||
});
|
||||
// Create fresh globals each time user code is run.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const globals = pyodide.globals.get('dict')() as PyProxy;
|
||||
// Some tests rely on __name__ being set to __main__ and we new dicts do not
|
||||
// have this set by default.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
globals.set('__name__', '__main__');
|
||||
|
||||
// The runPython helper is a shortcut for running python code with our
|
||||
// custom globals.
|
||||
const runPython = (pyCode: string) =>
|
||||
pyodide!.runPython(pyCode, { globals }) as unknown;
|
||||
|
||||
runPython(`
|
||||
from pyodide.ffi import JsException
|
||||
|
||||
import jscustom
|
||||
from jscustom import print
|
||||
from jscustom import input
|
||||
|
||||
def __wrap(func):
|
||||
def fn(*args):
|
||||
try:
|
||||
data = func(*args)
|
||||
if data.type == 'cancel':
|
||||
raise KeyboardInterrupt(data.value)
|
||||
return data.value
|
||||
except JsException:
|
||||
jscustom.__interruptExecution()
|
||||
raise
|
||||
return fn
|
||||
input = __wrap(input)
|
||||
`);
|
||||
|
||||
// Exposing sys.last_value can create memory leaks, so this just returns a
|
||||
// string instead of the actual exception. args[0] is what was passed to the
|
||||
// exception constructor. In our case, that's the id we want.
|
||||
// TODO: I'm using 'join' to make sure we're not leaking a reference to the
|
||||
// exception. This might be excessive, but I don't know enough about pyodide
|
||||
// to be sure.
|
||||
runPython(`
|
||||
import sys
|
||||
def __get_reset_id():
|
||||
if sys.last_value and sys.last_value.args:
|
||||
return "".join(str(sys.last_value.args[0]))
|
||||
else:
|
||||
return ""
|
||||
`);
|
||||
runPython(`
|
||||
def print_exception():
|
||||
from format_exception import format_exception
|
||||
formatted = format_exception(exception=sys.last_value, traceback=sys.last_traceback, filename="<exec>", new_filename="main.py")
|
||||
print(formatted)
|
||||
`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const printException = globals.get('print_exception') as PyProxy &
|
||||
(() => string);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const getResetId = globals.get('__get_reset_id') as PyProxy & (() => string);
|
||||
return { runPython, getResetId, globals, printException };
|
||||
}
|
||||
|
||||
ctx.onmessage = (e: PythonRunEvent | ListenRequestEvent | CancelEvent) => {
|
||||
const { data } = e;
|
||||
if (data.type === 'listen') {
|
||||
handleListenRequest();
|
||||
} else if (data.type === 'cancel') {
|
||||
handleCancelRequest(data);
|
||||
} else {
|
||||
handleRunRequest(data);
|
||||
}
|
||||
};
|
||||
|
||||
// This lets the client know that there is nothing to cancel.
|
||||
function handleCancelRequest({ value }: { value: number }) {
|
||||
postMessage({ type: 'is-alive', text: value });
|
||||
}
|
||||
|
||||
function handleListenRequest() {
|
||||
ignoreRunMessages = false;
|
||||
}
|
||||
|
||||
function handleRunRequest(data: PythonRunEvent['data']) {
|
||||
try {
|
||||
if (ignoreRunMessages) return;
|
||||
const code = (data.code.contents || '').slice();
|
||||
// TODO: use reset-terminal for clarity?
|
||||
postMessage({ type: 'reset' });
|
||||
|
||||
const { runPython, getResetId, globals, printException } = initRunPython();
|
||||
// use pyodide.runPythonAsync if we want top-level await
|
||||
try {
|
||||
runPython(code);
|
||||
} catch (e) {
|
||||
const err = e as PythonError;
|
||||
// the formatted exception is printed to the terminal
|
||||
printException();
|
||||
// but the full error is logged to the console for debugging
|
||||
console.error(err);
|
||||
const resetId = getResetId();
|
||||
// TODO: if a user raises a KeyboardInterrupt with a custom message this
|
||||
// will be treated as a reset, the client will resend their code and this
|
||||
// will loop. Can we fix that? Perhaps by using a custom exception?
|
||||
if (err.type === 'KeyboardInterrupt' && resetId) {
|
||||
// If the client sends a lot of run messages, it's easy for them to build
|
||||
// up while the worker is busy. As such, we both ignore any queued run
|
||||
// messages...
|
||||
ignoreRunMessages = true;
|
||||
// ...and tell the client that we're ignoring them.
|
||||
postMessage({ type: 'stopped', text: getResetId() });
|
||||
}
|
||||
} finally {
|
||||
getResetId.destroy();
|
||||
printException.destroy();
|
||||
globals.destroy();
|
||||
}
|
||||
} catch (e) {
|
||||
// This should only be reach if pyodide crashes, but it's helpful to log
|
||||
// the error in case it's something else.
|
||||
console.error(e);
|
||||
void resetPyodide();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { version } from '@freecodecamp/browser-scripts/package.json';
|
||||
// work around for SASS error in Edge
|
||||
// https://github.com/medialize/sass.js/issues/96#issuecomment-424386171
|
||||
interface WorkerWithSass extends Worker {
|
||||
Sass: {
|
||||
compile(data: unknown, callback: unknown): void;
|
||||
};
|
||||
}
|
||||
|
||||
const ctx: WorkerWithSass & typeof globalThis =
|
||||
self as unknown as WorkerWithSass & typeof globalThis;
|
||||
|
||||
if (!ctx.crypto) {
|
||||
(ctx.crypto as unknown) = {
|
||||
getRandomValues: function (array: number[]) {
|
||||
for (let i = 0, l = array.length; i < l; i++) {
|
||||
array[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ctx.importScripts(`/js/workers/${version}/sass.sync.js`);
|
||||
|
||||
ctx.onmessage = e => {
|
||||
const data: unknown = e.data;
|
||||
ctx.Sass.compile(data, (result: Record<string, unknown>) => {
|
||||
if (result.status === 0) {
|
||||
ctx.postMessage(result.text);
|
||||
} else {
|
||||
ctx.postMessage({
|
||||
type: 'error',
|
||||
data: { message: result.formatted }
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ctx.postMessage({ type: 'contentLoaded' });
|
||||
@@ -0,0 +1,3 @@
|
||||
export type { FCCTestRunner } from '@freecodecamp/curriculum-helpers/test-runner.js';
|
||||
|
||||
export { version } from '@freecodecamp/curriculum-helpers/package.json';
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["WebWorker", "DOM"],
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"pyodide/ffi": ["./node_modules/pyodide/ffi.d.ts"] // Remove after updating pyodide (later versions correctly export ffi)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Compiler } from './modules/typescript-compiler';
|
||||
|
||||
// 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
|
||||
// challenge (in general - it will be hardcoded in the MVP). So, the vfs types
|
||||
// should be correct, but ts may not be.
|
||||
declare const tsvfs: typeof import('@typescript/vfs');
|
||||
declare const ts: typeof import('typescript');
|
||||
|
||||
const ctx: Worker & typeof globalThis = self as unknown as Worker &
|
||||
typeof globalThis;
|
||||
|
||||
interface TSCompileEvent extends MessageEvent {
|
||||
data: {
|
||||
type: 'compile';
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface TSCompiledMessage {
|
||||
type: 'compiled';
|
||||
value: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface SetupEvent extends MessageEvent {
|
||||
data: {
|
||||
type: 'setup';
|
||||
tsconfig?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CancelEvent extends MessageEvent {
|
||||
data: {
|
||||
type: 'cancel';
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Pin at the latest TS version available as cdnjs doesn't support version range.
|
||||
const TS_VERSION = '5.9.2';
|
||||
|
||||
let cachedVersion: string | null = null;
|
||||
|
||||
// NOTE: vfs.globals must only be imported once, otherwise it will throw.
|
||||
importScripts(
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/typescript-vfs/1.6.1/vfs.globals.js'
|
||||
);
|
||||
|
||||
function importTS(version: string) {
|
||||
if (cachedVersion == version) return;
|
||||
importScripts(
|
||||
/* typescript.min.js fails with
|
||||
|
||||
typescript.min.js:320 Uncaught TypeError: Class constructors cannot be invoked without 'new'
|
||||
|
||||
so we're using the non-minified version for now. */
|
||||
`https://cdnjs.cloudflare.com/ajax/libs/typescript/${version}/typescript.js`
|
||||
);
|
||||
cachedVersion = version;
|
||||
}
|
||||
|
||||
ctx.onmessage = (e: TSCompileEvent | SetupEvent | CancelEvent) => {
|
||||
const { data, ports } = e;
|
||||
if (data.type === 'setup') {
|
||||
void handleSetupRequest(data, ports[0]);
|
||||
} else if (data.type === 'cancel') {
|
||||
handleCancelRequest(data);
|
||||
} else {
|
||||
handleCompileRequest(data, ports[0]);
|
||||
}
|
||||
};
|
||||
|
||||
importTS(TS_VERSION);
|
||||
|
||||
const compiler = new Compiler(ts, tsvfs);
|
||||
|
||||
// This lets the client know that there is nothing to cancel.
|
||||
function handleCancelRequest({ value }: { value: number }) {
|
||||
postMessage({ type: 'is-alive', text: value });
|
||||
}
|
||||
|
||||
async function handleSetupRequest(data: SetupEvent['data'], port: MessagePort) {
|
||||
await compiler.setup({
|
||||
tsconfig: data.tsconfig
|
||||
});
|
||||
// We freeze this to prevent learners from getting the worker into a weird
|
||||
// state.
|
||||
Object.freeze(self);
|
||||
|
||||
port.postMessage({ type: 'ready' });
|
||||
}
|
||||
|
||||
function handleCompileRequest(data: TSCompileEvent['data'], port: MessagePort) {
|
||||
const { result, error } = compiler.compile(data.code, 'index.tsx');
|
||||
const message: TSCompiledMessage = {
|
||||
type: 'compiled',
|
||||
value: result,
|
||||
error: error
|
||||
};
|
||||
|
||||
port.postMessage(message);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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.js';
|
||||
|
||||
export function format(x) {
|
||||
// we're trying to mimic console.log, so we avoid wrapping strings in quotes:
|
||||
if (typeof x === 'string') return x;
|
||||
else if (x instanceof Set) {
|
||||
return `Set(${x.size}) {${Array.from(x).join(', ')}}`;
|
||||
} else if (x instanceof Map) {
|
||||
return `Map(${x.size}) {${Array.from(
|
||||
x.entries(),
|
||||
([k, v]) => `${k} => ${v}`
|
||||
).join(', ')}})`;
|
||||
} else if (typeof x === 'bigint') {
|
||||
return x.toString() + 'n';
|
||||
} else if (typeof x === 'symbol') {
|
||||
return x.toString();
|
||||
}
|
||||
return inspect(x);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const { version } = require('./package.json');
|
||||
|
||||
module.exports = (env = {}) => {
|
||||
const __DEV__ = env.production !== true;
|
||||
|
||||
return {
|
||||
cache: __DEV__ ? { type: 'filesystem' } : false,
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
entry: {
|
||||
'sass-compile': './sass-compile.ts',
|
||||
'python-worker': './python-worker.ts',
|
||||
'typescript-worker': './typescript-worker.ts'
|
||||
},
|
||||
devtool: __DEV__ ? 'inline-source-map' : 'source-map',
|
||||
output: {
|
||||
chunkFilename: '[name]-[contenthash].js',
|
||||
path: path.resolve(__dirname, `dist/js/workers/${version}`),
|
||||
clean: false // We handle cleaning in copy-scripts.ts
|
||||
},
|
||||
stats: {
|
||||
// Display bailout reasons
|
||||
optimizationBailout: true
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|ts)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
babelrc: false,
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{ modules: false, targets: '> 0.25%, not dead' }
|
||||
],
|
||||
'@babel/preset-typescript'
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-transform-runtime',
|
||||
'@babel/plugin-syntax-dynamic-import'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser'
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer']
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
fallback: {
|
||||
buffer: require.resolve('buffer'),
|
||||
util: require.resolve('util'),
|
||||
stream: false,
|
||||
process: require.resolve('process/browser.js')
|
||||
},
|
||||
extensions: ['.js', '.ts']
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user