Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion app/terminal/exec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,16 @@ import { useEmbedContext } from "./embedContext";
import clsx from "clsx";
import { LangConstants } from "@my-code/runtime/languages";
import { useRuntime } from "@my-code/runtime/context";
import { captureException } from "@sentry/nextjs";
import { MinMaxButton, Modal } from "./modal";

function handleRuntimeError(error: unknown) {
captureException(error);
window.alert(
"コード実行環境で予期せぬエラーが発生しました: \n" + String(error)
);
}

interface ExecProps {
/*
* Pythonの場合はメインファイル1つのみを指定する。
Expand Down Expand Up @@ -68,8 +76,9 @@ export function ExecFile(props: ExecProps) {
`Language ${props.language.originalLang} does not have a runtime environment.`
);
}

const { ready, runFiles, getCommandlineStr, runtimeInfo, interrupt } =
useRuntime(props.language.runtime);
useRuntime(props.language.runtime, { onError: handleRuntimeError });

// ユーザーがクリックした時(triggered) && ランタイムが準備できた時に、実際にinitCommandを実行する(executing)
const [executionState, setExecutionState] = useState<
Expand Down
11 changes: 10 additions & 1 deletion app/terminal/repl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useEmbedContext } from "./embedContext";
import { LangConstants } from "@my-code/runtime/languages";
import clsx from "clsx";
import { InlineCode } from "@/markdown/codeBlock";
import { captureException } from "@sentry/nextjs";
import {
emptyMutex,
ReplCommand,
Expand All @@ -26,6 +27,13 @@ import { useRuntime } from "@my-code/runtime/context";
import { MinMaxButton, Modal } from "./modal";
import { StopButtonContent } from "./exec";

function handleRuntimeError(error: unknown) {
captureException(error);
window.alert(
"コード実行環境で予期せぬエラーが発生しました: \n" + String(error)
);
}

export function writeOutput(
term: Terminal,
output: ReplOutput,
Expand All @@ -37,6 +45,7 @@ export function writeOutput(
const message = String(output.message).replace(/\n/g, "\r\n");
switch (output.type) {
case "error":
case "fatalError":
term.writeln(chalk.red(message));
break;
case "trace":
Expand Down Expand Up @@ -95,7 +104,7 @@ export function ReplTerminal({
checkSyntax,
splitReplExamples,
runtimeInfo,
} = useRuntime(language.runtime);
} = useRuntime(language.runtime, { onError: handleRuntimeError });
const { tabSize, prompt, promptMore, returnPrefix } = language;
if (!prompt) {
console.warn(`prompt not defined for language: ${language}`);
Expand Down
13 changes: 10 additions & 3 deletions packages/runtime/src/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@ import { PyodideContext, usePyodide } from "./worker/pyodide";
import { RubyContext, useRuby } from "./worker/ruby";
import { WorkerProvider } from "./worker/runtime";

export function useRuntime(language: RuntimeLang): RuntimeContext {
interface UseRuntimeOptions {
onError?: (error: unknown) => void;
}
export function useRuntime(
language: RuntimeLang,
options?: UseRuntimeOptions
): RuntimeContext {
const runtimes = useRuntimeAll();
const runtime = runtimes[language];
const { init } = runtime;
const { onError } = options ?? {};
useEffect(() => {
init?.();
}, [init]);
init?.(onError);
}, [init, onError]);
return runtime;
}
export function useRuntimeAll(): Record<RuntimeLang, RuntimeContext> {
Expand Down
4 changes: 3 additions & 1 deletion packages/runtime/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface RuntimeContext {
* 初期化とcleanupはuseEffect()で非同期に行うのがよいです。
*
*/
init?: () => void;
init?: (onError?: RuntimeErrorHandler) => void;
/**
* ランタイムの初期化が完了したか、不要である場合true
*/
Expand Down Expand Up @@ -148,11 +148,13 @@ export interface RuntimeInfo {
prettyLangName: string;
version?: string;
}
export type RuntimeErrorHandler = (error: unknown) => void;

export const ReplOutputTypeSchema = z.enum([
"stdout",
"stderr",
"error",
"fatalError",
"return",
"trace",
"system",
Expand Down
42 changes: 34 additions & 8 deletions packages/runtime/src/typescript/runtime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ import {
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { ReplOutput, RuntimeContext, RuntimeInfo, UpdatedFile } from "../interface";
import {
ReplOutput,
RuntimeContext,
RuntimeErrorHandler,
RuntimeInfo,
UpdatedFile,
} from "../interface";

export const compilerOptions: CompilerOptions = {
lib: ["ESNext", "WebWorker"],
Expand All @@ -20,22 +27,26 @@ export const compilerOptions: CompilerOptions = {
};

const TypeScriptContext = createContext<{
init: () => void;
init: (onError?: RuntimeErrorHandler) => void;
tsEnv: VirtualTypeScriptEnvironment | null;
tsVersion?: string;
}>({ init: () => undefined, tsEnv: null });
export function TypeScriptProvider({ children }: { children: ReactNode }) {
const [tsEnv, setTSEnv] = useState<VirtualTypeScriptEnvironment | null>(null);
const [tsVersion, setTSVersion] = useState<string | undefined>(undefined);
const [doInit, setDoInit] = useState(false);
const init = useCallback(() => setDoInit(true), []);
const onErrorRef = useRef<RuntimeErrorHandler | undefined>(undefined);
const init = useCallback((onError?: RuntimeErrorHandler) => {
onErrorRef.current = onError;
setDoInit(true);
}, []);
useEffect(() => {
// useEffectはサーバーサイドでは実行されないが、
// typeof window !== "undefined" でガードしないとなぜかesbuildが"typescript"を
// サーバーサイドでのインポート対象とみなしてしまう。
if (doInit && tsEnv === null && typeof window !== "undefined") {
const abortController = new AbortController();
(async () => {
void (async () => {
const ts = await import("typescript");
const vfs = await import("@typescript/vfs");
const system = vfs.createSystem(new Map());
Expand Down Expand Up @@ -70,7 +81,12 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) {
);
setTSEnv(env);
setTSVersion(ts.version);
})();
})().catch((error) => {
if (error instanceof DOMException && error.name === "AbortError") {
return;
}
onErrorRef.current?.(error);
});
return () => {
abortController.abort();
};
Expand All @@ -86,9 +102,11 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) {
export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {
const { init: tsInit, tsEnv, tsVersion } = useContext(TypeScriptContext);
const { init: jsInit } = jsEval;
const init = useCallback(() => {
tsInit();
jsInit?.();
const onErrorRef = useRef<RuntimeErrorHandler | undefined>(undefined);
const init = useCallback((onError?: RuntimeErrorHandler) => {
onErrorRef.current = onError;
tsInit(onError);
jsInit?.(onError);
}, [tsInit, jsInit]);

const runFiles = useCallback(
Expand All @@ -101,6 +119,7 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {
onOutput({ type: "error", message: "TypeScript is not ready yet." });
return;
} else {
try {
for (const [filename, content] of Object.entries(files)) {
tsEnv.createFile(filename, content);
}
Expand Down Expand Up @@ -151,6 +170,13 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {
{ ...files, ...emittedFiles },
onOutput
);
} catch (error) {
onErrorRef.current?.(error);
onOutput({
type: "fatalError",
message: error instanceof Error ? error.message : String(error),
});
}
}
},
[tsEnv, jsEval]
Expand Down
59 changes: 45 additions & 14 deletions packages/runtime/src/wandbox/runtime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,27 @@ import {
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
} from "react";
import useSWR from "swr";
import { compilerInfoFetcher, SelectedCompiler } from "./api";
import { cppRunFiles, selectCppCompiler } from "./cpp";
import { RuntimeLang } from "../languages";
import { rustRunFiles, selectRustCompiler } from "./rust";
import { ReplOutput, RuntimeContext, RuntimeInfo, UpdatedFile } from "../interface";
import {
ReplOutput,
RuntimeContext,
RuntimeErrorHandler,
RuntimeInfo,
UpdatedFile,
} from "../interface";

type WandboxLang = "cpp" | "rust";

interface IWandboxContext {
init: (onError?: RuntimeErrorHandler) => void;
ready: boolean;
getCommandlineStrWithLang: (
lang: WandboxLang
Expand All @@ -34,10 +43,17 @@ interface IWandboxContext {
const WandboxContext = createContext<IWandboxContext>(null!);

export function WandboxProvider({ children }: { children: ReactNode }) {
const onErrorRef = useRef<RuntimeErrorHandler | undefined>(undefined);
const init = useCallback((onError?: RuntimeErrorHandler) => {
onErrorRef.current = onError;
}, []);
const { data: compilerList, error } = useSWR("list", compilerInfoFetcher);
if (error) {
console.error("Failed to fetch compiler list from Wandbox:", error);
}
useEffect(() => {
if (error) {
console.error("Failed to fetch compiler list from Wandbox:", error);
onErrorRef.current?.(error);
}
}, [error]);

const ready = !!compilerList;

Expand Down Expand Up @@ -76,16 +92,29 @@ export function WandboxProvider({ children }: { children: ReactNode }) {
onOutput({ type: "error", message: "Wandbox is not ready yet." });
return;
}
switch (lang) {
case "cpp":
await cppRunFiles(selectedCompiler.cpp, files, filenames, onOutput);
break;
case "rust":
await rustRunFiles(selectedCompiler.rust, files, filenames, onOutput);
break;
default:
lang satisfies never;
throw new Error(`unsupported language: ${lang}`);
try {
switch (lang) {
case "cpp":
await cppRunFiles(selectedCompiler.cpp, files, filenames, onOutput);
break;
case "rust":
await rustRunFiles(
selectedCompiler.rust,
files,
filenames,
onOutput
);
break;
default:
lang satisfies never;
throw new Error(`unsupported language: ${lang}`);
}
} catch (error) {
onErrorRef.current?.(error);
onOutput({
type: "fatalError",
message: error instanceof Error ? error.message : String(error),
});
}
},
[selectedCompiler]
Expand All @@ -94,6 +123,7 @@ export function WandboxProvider({ children }: { children: ReactNode }) {
return (
<WandboxContext.Provider
value={{
init,
ready,
getCommandlineStrWithLang,
runFilesWithLang,
Expand Down Expand Up @@ -123,6 +153,7 @@ export function useWandbox(lang: WandboxLang): RuntimeContext {
);

return {
init: context.init,
ready: context.ready,
runFiles,
getCommandlineStr,
Expand Down
8 changes: 4 additions & 4 deletions packages/runtime/src/worker/pyodide.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,13 @@ async function runCode(
});
} else {
await onOutput({
type: "error",
type: "fatalError",
message: `予期せぬエラー: ${e.message.trim()}`,
});
}
} else {
await onOutput({
type: "error",
type: "fatalError",
message: `予期せぬエラー: ${String(e).trim()}`,
});
}
Expand Down Expand Up @@ -175,13 +175,13 @@ async function runFile(
});
} else {
await onOutput({
type: "error",
type: "fatalError",
message: `予期せぬエラー: ${e.message.trim()}`,
});
}
} else {
await onOutput({
type: "error",
type: "fatalError",
message: `予期せぬエラー: ${String(e).trim()}`,
});
}
Expand Down
Loading
Loading