Skip to content
6 changes: 5 additions & 1 deletion app/terminal/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const AceEditor = dynamic(
await import("ace-builds/src-min-noconflict/ext-searchbox");
await import("ace-builds/src-min-noconflict/mode-python");
await import("ace-builds/src-min-noconflict/mode-c_cpp");
await import("ace-builds/src-min-noconflict/mode-javascript");
await import("ace-builds/src-min-noconflict/mode-json");
await import("ace-builds/src-min-noconflict/mode-csv");
await import("ace-builds/src-min-noconflict/mode-text");
Expand All @@ -28,7 +29,7 @@ import { langConstants } from "./runtime";
// snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python";

// mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値が対応する
export type AceLang = "python" | "c_cpp" | "json" | "csv" | "text";
export type AceLang = "python" | "c_cpp" | "javascript" | "json" | "csv" | "text";
export function getAceLang(lang: string | undefined): AceLang {
// Markdownで指定される可能性のある言語名からAceLangを取得
switch (lang) {
Expand All @@ -38,6 +39,9 @@ export function getAceLang(lang: string | undefined): AceLang {
case "cpp":
case "c++":
return "c_cpp";
case "javascript":
case "js":
return "javascript";
case "json":
return "json";
case "csv":
Expand Down
5 changes: 4 additions & 1 deletion app/terminal/highlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import chalk from "chalk";
import { RuntimeLang } from "./runtime";
// Python言語定義をインポート
import "prismjs/components/prism-python";
import "prismjs/components/prism-javascript";

type PrismLang = "python";
type PrismLang = "python" | "javascript";

function getPrismLanguage(language: RuntimeLang): PrismLang {
switch (language) {
case "python":
return "python";
case "javascript":
return "javascript";
case "cpp":
throw new Error(
`highlight for ${language} is disabled because it should not support REPL`
Expand Down
21 changes: 21 additions & 0 deletions app/terminal/javascript/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";

import { EditorComponent } from "../editor";
import { ReplTerminal } from "../repl";

export default function JavaScriptPage() {
return (
<div className="p-4 flex flex-col gap-4">
<ReplTerminal
terminalId=""
language="javascript"
initContent={"> console.log('hello, world!')\nhello, world!"}
/>
<EditorComponent
language="javascript"
filename="main.js"
initContent="console.log('hello, world!');"
/>
</div>
);
}
238 changes: 238 additions & 0 deletions app/terminal/javascript/runtime.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
"use client";

import {
useState,
useRef,
useCallback,
ReactNode,
createContext,
useContext,
useEffect,
} from "react";
import { SyntaxStatus, ReplOutput, ReplCommand } from "../repl";
import { Mutex, MutexInterface } from "async-mutex";
import { RuntimeContext } from "../runtime";

const JavaScriptContext = createContext<RuntimeContext>(null!);

export function useJavaScript(): RuntimeContext {
const context = useContext(JavaScriptContext);
if (!context) {
throw new Error("useJavaScript must be used within a JavaScriptProvider");
}
return context;
}

type MessageToWorker =
| {
type: "init";
payload?: undefined;
}
| {
type: "runJavaScript";
payload: { code: string };
}
| {
type: "checkSyntax";
payload: { code: string };
}
| {
type: "restoreState";
payload: { commands: string[] };
};

type MessageFromWorker =
| { id: number; payload: unknown }
| { id: number; error: string };

type InitPayloadFromWorker = { success: boolean };
type RunPayloadFromWorker = {
output: ReplOutput[];
updatedFiles: [string, string][];
};
type StatusPayloadFromWorker = { status: SyntaxStatus };

export function JavaScriptProvider({ children }: { children: ReactNode }) {
const workerRef = useRef<Worker | null>(null);
const [ready, setReady] = useState<boolean>(false);
const mutex = useRef<MutexInterface>(new Mutex());
const messageCallbacks = useRef<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Map<number, [(payload: any) => void, (error: string) => void]>
>(new Map());
const nextMessageId = useRef<number>(0);
const executedCommands = useRef<string[]>([]);

function postMessage<T>({ type, payload }: MessageToWorker) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot
error: Property 'payload' does not exist on type 'MessageToWorker'.

initとrestorestateメッセージに空のpayloadを追加すると解決すると思います。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bdfa2a1. Added payload?: undefined to init type and payload: { commands: string[] } to restoreState type.

const id = nextMessageId.current++;
return new Promise<T>((resolve, reject) => {
messageCallbacks.current.set(id, [resolve, reject]);
workerRef.current?.postMessage({ id, type, payload });
});
}

const initializeWorker = useCallback(() => {
const worker = new Worker("/javascript.worker.js");
workerRef.current = worker;

worker.onmessage = (event) => {
const data = event.data as MessageFromWorker;
if (messageCallbacks.current.has(data.id)) {
const [resolve, reject] = messageCallbacks.current.get(data.id)!;
if ("error" in data) {
reject(data.error);
} else {
resolve(data.payload);
}
messageCallbacks.current.delete(data.id);
}
};

return postMessage<InitPayloadFromWorker>({
type: "init",
}).then(({ success }) => {
if (success) {
setReady(true);
}
return worker;
});
}, []);

useEffect(() => {
let worker: Worker | null = null;
initializeWorker().then((w) => {
worker = w;
});

return () => {
worker?.terminate();
};
}, [initializeWorker]);

const interrupt = useCallback(() => {
// Since we can't interrupt JavaScript execution directly,
// we terminate the worker and restart it, then restore state

// Reject all pending callbacks before terminating
const error = "Worker interrupted";
messageCallbacks.current.forEach(([, reject]) => reject(error));
messageCallbacks.current.clear();

// Terminate the current worker
workerRef.current?.terminate();

// Reset ready state
setReady(false);

mutex.current.runExclusive(async () => {
// Create a new worker and wait for it to be ready
await initializeWorker();

// Restore state by re-executing previous commands
if (executedCommands.current.length > 0) {
await postMessage<{ success: boolean }>({
type: "restoreState",
payload: { commands: executedCommands.current },
});
}
});
}, [initializeWorker]);

const runCommand = useCallback(
async (code: string): Promise<ReplOutput[]> => {
if (!mutex.current.isLocked()) {
throw new Error(
"mutex of JavaScriptContext must be locked for runCommand"
);
}
if (!workerRef.current || !ready) {
return [{ type: "error", message: "JavaScript runtime is not ready yet." }];
}

try {
const { output } = await postMessage<RunPayloadFromWorker>({
type: "runJavaScript",
payload: { code },
});
// Save successfully executed command
executedCommands.current.push(code);
return output;
} catch (error) {
// Handle errors (including "Worker interrupted")
if (error instanceof Error) {
return [{ type: "error", message: error.message }];
}
return [{ type: "error", message: String(error) }];
}
},
[ready]
);

const checkSyntax = useCallback(
async (code: string): Promise<SyntaxStatus> => {
if (!workerRef.current || !ready) return "invalid";
const { status } = await mutex.current.runExclusive(() =>
postMessage<StatusPayloadFromWorker>({
type: "checkSyntax",
payload: { code },
})
);
return status;
},
[ready]
);

const runFiles = useCallback(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async (_filenames: string[]): Promise<ReplOutput[]> => {
return [
{
type: "error",
message: "JavaScript file execution is not supported in this runtime",
},
];
},
[]
);

const splitReplExamples = useCallback((content: string): ReplCommand[] => {
const initCommands: { command: string; output: ReplOutput[] }[] = [];
for (const line of content.split("\n")) {
if (line.startsWith("> ")) {
// Remove the prompt from the command
initCommands.push({ command: line.slice(2), output: [] });
} else {
// Lines without prompt are output from the previous command
if (initCommands.length > 0) {
initCommands[initCommands.length - 1].output.push({
type: "stdout",
message: line,
});
}
}
}
return initCommands;
}, []);

const getCommandlineStr = useCallback(
(filenames: string[]) => `node ${filenames[0]}`,
[]
);

return (
<JavaScriptContext.Provider
value={{
ready,
runCommand,
checkSyntax,
mutex: mutex.current,
runFiles,
interrupt,
splitReplExamples,
getCommandlineStr,
}}
>
{children}
</JavaScriptContext.Provider>
);
}
18 changes: 16 additions & 2 deletions app/terminal/runtime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { MutexInterface } from "async-mutex";
import { ReplOutput, SyntaxStatus, ReplCommand } from "./repl";
import { PyodideProvider, usePyodide } from "./python/runtime";
import { useWandbox, WandboxProvider } from "./wandbox/runtime";
import { JavaScriptProvider, useJavaScript } from "./javascript/runtime";
import { AceLang } from "./editor";
import { ReactNode } from "react";

Expand All @@ -28,7 +29,7 @@ export interface LangConstants {
prompt?: string;
promptMore?: string;
}
export type RuntimeLang = "python" | "cpp";
export type RuntimeLang = "python" | "cpp" | "javascript";

export function getRuntimeLang(
lang: string | undefined
Expand All @@ -41,6 +42,9 @@ export function getRuntimeLang(
case "cpp":
case "c++":
return "cpp";
case "javascript":
case "js":
return "javascript";
default:
console.warn(`Unsupported language for runtime: ${lang}`);
return undefined;
Expand All @@ -50,12 +54,15 @@ export function useRuntime(language: RuntimeLang): RuntimeContext {
// すべての言語のcontextをインスタンス化
const pyodide = usePyodide();
const wandboxCpp = useWandbox("cpp");
const javascript = useJavaScript();

switch (language) {
case "python":
return pyodide;
case "cpp":
return wandboxCpp;
case "javascript":
return javascript;
default:
language satisfies never;
throw new Error(`Runtime not implemented for language: ${language}`);
Expand All @@ -64,7 +71,9 @@ export function useRuntime(language: RuntimeLang): RuntimeContext {
export function RuntimeProvider({ children }: { children: ReactNode }) {
return (
<PyodideProvider>
<WandboxProvider>{children}</WandboxProvider>
<WandboxProvider>
<JavaScriptProvider>{children}</JavaScriptProvider>
</WandboxProvider>
</PyodideProvider>
);
}
Expand All @@ -77,6 +86,11 @@ export function langConstants(lang: RuntimeLang | AceLang): LangConstants {
prompt: ">>> ",
promptMore: "... ",
};
case "javascript":
return {
tabSize: 2,
prompt: "> ",
};
case "c_cpp":
case "cpp":
return {
Expand Down
Loading
Loading