Skip to content

Commit 2f11ed3

Browse files
committed
Merge remote-tracking branch 'origin/copilot/add-js-runtime-api' into refactor-worker-runtime
2 parents 8b04d28 + 35244c8 commit 2f11ed3

File tree

8 files changed

+429
-13
lines changed

8 files changed

+429
-13
lines changed

app/terminal/editor.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const AceEditor = dynamic(
1313
await import("ace-builds/src-min-noconflict/mode-python");
1414
await import("ace-builds/src-min-noconflict/mode-ruby");
1515
await import("ace-builds/src-min-noconflict/mode-c_cpp");
16+
await import("ace-builds/src-min-noconflict/mode-javascript");
1617
await import("ace-builds/src-min-noconflict/mode-json");
1718
await import("ace-builds/src-min-noconflict/mode-csv");
1819
await import("ace-builds/src-min-noconflict/mode-text");
@@ -29,7 +30,7 @@ import { langConstants } from "./runtime";
2930
// snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python";
3031

3132
// mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値が対応する
32-
export type AceLang = "python" | "ruby" | "c_cpp" | "json" | "csv" | "text";
33+
export type AceLang = "python" | "ruby" | "c_cpp" | "javascript" | "json" | "csv" | "text";
3334
export function getAceLang(lang: string | undefined): AceLang {
3435
// Markdownで指定される可能性のある言語名からAceLangを取得
3536
switch (lang) {
@@ -42,6 +43,9 @@ export function getAceLang(lang: string | undefined): AceLang {
4243
case "cpp":
4344
case "c++":
4445
return "c_cpp";
46+
case "javascript":
47+
case "js":
48+
return "javascript";
4549
case "json":
4650
return "json";
4751
case "csv":

app/terminal/highlight.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import Prism from "prismjs";
22
import chalk from "chalk";
33
import { RuntimeLang } from "./runtime";
4-
// Python言語定義をインポート
4+
// 言語定義をインポート
55
import "prismjs/components/prism-python";
6-
// Ruby言語定義をインポート
76
import "prismjs/components/prism-ruby";
7+
import "prismjs/components/prism-javascript";
88

9-
type PrismLang = "python" | "ruby";
9+
type PrismLang = "python" | "ruby" | "javascript";
1010

1111
function getPrismLanguage(language: RuntimeLang): PrismLang {
1212
switch (language) {
1313
case "python":
1414
return "python";
1515
case "ruby":
1616
return "ruby";
17+
case "javascript":
18+
return "javascript";
1719
case "cpp":
1820
throw new Error(
1921
`highlight for ${language} is disabled because it should not support REPL`

app/terminal/javascript/page.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"use client";
2+
3+
import { EditorComponent } from "../editor";
4+
import { ReplTerminal } from "../repl";
5+
6+
export default function JavaScriptPage() {
7+
return (
8+
<div className="p-4 flex flex-col gap-4">
9+
<ReplTerminal
10+
terminalId=""
11+
language="javascript"
12+
initContent={"> console.log('hello, world!')\nhello, world!"}
13+
/>
14+
<EditorComponent
15+
language="javascript"
16+
filename="main.js"
17+
initContent="console.log('hello, world!');"
18+
/>
19+
</div>
20+
);
21+
}
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
"use client";
2+
3+
import {
4+
useState,
5+
useRef,
6+
useCallback,
7+
ReactNode,
8+
createContext,
9+
useContext,
10+
useEffect,
11+
} from "react";
12+
import { SyntaxStatus, ReplOutput, ReplCommand } from "../repl";
13+
import { Mutex, MutexInterface } from "async-mutex";
14+
import { RuntimeContext } from "../runtime";
15+
16+
const JavaScriptContext = createContext<RuntimeContext>(null!);
17+
18+
export function useJavaScript(): RuntimeContext {
19+
const context = useContext(JavaScriptContext);
20+
if (!context) {
21+
throw new Error("useJavaScript must be used within a JavaScriptProvider");
22+
}
23+
return context;
24+
}
25+
26+
type MessageToWorker =
27+
| {
28+
type: "init";
29+
payload?: undefined;
30+
}
31+
| {
32+
type: "runJavaScript";
33+
payload: { code: string };
34+
}
35+
| {
36+
type: "checkSyntax";
37+
payload: { code: string };
38+
}
39+
| {
40+
type: "restoreState";
41+
payload: { commands: string[] };
42+
};
43+
44+
type MessageFromWorker =
45+
| { id: number; payload: unknown }
46+
| { id: number; error: string };
47+
48+
type InitPayloadFromWorker = { success: boolean };
49+
type RunPayloadFromWorker = {
50+
output: ReplOutput[];
51+
updatedFiles: [string, string][];
52+
};
53+
type StatusPayloadFromWorker = { status: SyntaxStatus };
54+
55+
export function JavaScriptProvider({ children }: { children: ReactNode }) {
56+
const workerRef = useRef<Worker | null>(null);
57+
const [ready, setReady] = useState<boolean>(false);
58+
const mutex = useRef<MutexInterface>(new Mutex());
59+
const messageCallbacks = useRef<
60+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
61+
Map<number, [(payload: any) => void, (error: string) => void]>
62+
>(new Map());
63+
const nextMessageId = useRef<number>(0);
64+
const executedCommands = useRef<string[]>([]);
65+
66+
function postMessage<T>({ type, payload }: MessageToWorker) {
67+
const id = nextMessageId.current++;
68+
return new Promise<T>((resolve, reject) => {
69+
messageCallbacks.current.set(id, [resolve, reject]);
70+
workerRef.current?.postMessage({ id, type, payload });
71+
});
72+
}
73+
74+
const initializeWorker = useCallback(() => {
75+
const worker = new Worker("/javascript.worker.js");
76+
workerRef.current = worker;
77+
78+
worker.onmessage = (event) => {
79+
const data = event.data as MessageFromWorker;
80+
if (messageCallbacks.current.has(data.id)) {
81+
const [resolve, reject] = messageCallbacks.current.get(data.id)!;
82+
if ("error" in data) {
83+
reject(data.error);
84+
} else {
85+
resolve(data.payload);
86+
}
87+
messageCallbacks.current.delete(data.id);
88+
}
89+
};
90+
91+
return postMessage<InitPayloadFromWorker>({
92+
type: "init",
93+
}).then(({ success }) => {
94+
if (success) {
95+
setReady(true);
96+
}
97+
return worker;
98+
});
99+
}, []);
100+
101+
useEffect(() => {
102+
let worker: Worker | null = null;
103+
initializeWorker().then((w) => {
104+
worker = w;
105+
});
106+
107+
return () => {
108+
worker?.terminate();
109+
};
110+
}, [initializeWorker]);
111+
112+
const interrupt = useCallback(() => {
113+
// Since we can't interrupt JavaScript execution directly,
114+
// we terminate the worker and restart it, then restore state
115+
116+
// Reject all pending callbacks before terminating
117+
const error = "Worker interrupted";
118+
messageCallbacks.current.forEach(([, reject]) => reject(error));
119+
messageCallbacks.current.clear();
120+
121+
// Terminate the current worker
122+
workerRef.current?.terminate();
123+
124+
// Reset ready state
125+
setReady(false);
126+
127+
mutex.current.runExclusive(async () => {
128+
// Create a new worker and wait for it to be ready
129+
await initializeWorker();
130+
131+
// Restore state by re-executing previous commands
132+
if (executedCommands.current.length > 0) {
133+
await postMessage<{ success: boolean }>({
134+
type: "restoreState",
135+
payload: { commands: executedCommands.current },
136+
});
137+
}
138+
});
139+
}, [initializeWorker]);
140+
141+
const runCommand = useCallback(
142+
async (code: string): Promise<ReplOutput[]> => {
143+
if (!mutex.current.isLocked()) {
144+
throw new Error(
145+
"mutex of JavaScriptContext must be locked for runCommand"
146+
);
147+
}
148+
if (!workerRef.current || !ready) {
149+
return [{ type: "error", message: "JavaScript runtime is not ready yet." }];
150+
}
151+
152+
try {
153+
const { output } = await postMessage<RunPayloadFromWorker>({
154+
type: "runJavaScript",
155+
payload: { code },
156+
});
157+
// Save successfully executed command
158+
executedCommands.current.push(code);
159+
return output;
160+
} catch (error) {
161+
// Handle errors (including "Worker interrupted")
162+
if (error instanceof Error) {
163+
return [{ type: "error", message: error.message }];
164+
}
165+
return [{ type: "error", message: String(error) }];
166+
}
167+
},
168+
[ready]
169+
);
170+
171+
const checkSyntax = useCallback(
172+
async (code: string): Promise<SyntaxStatus> => {
173+
if (!workerRef.current || !ready) return "invalid";
174+
const { status } = await mutex.current.runExclusive(() =>
175+
postMessage<StatusPayloadFromWorker>({
176+
type: "checkSyntax",
177+
payload: { code },
178+
})
179+
);
180+
return status;
181+
},
182+
[ready]
183+
);
184+
185+
const runFiles = useCallback(
186+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
187+
async (_filenames: string[]): Promise<ReplOutput[]> => {
188+
return [
189+
{
190+
type: "error",
191+
message: "JavaScript file execution is not supported in this runtime",
192+
},
193+
];
194+
},
195+
[]
196+
);
197+
198+
const splitReplExamples = useCallback((content: string): ReplCommand[] => {
199+
const initCommands: { command: string; output: ReplOutput[] }[] = [];
200+
for (const line of content.split("\n")) {
201+
if (line.startsWith("> ")) {
202+
// Remove the prompt from the command
203+
initCommands.push({ command: line.slice(2), output: [] });
204+
} else {
205+
// Lines without prompt are output from the previous command
206+
if (initCommands.length > 0) {
207+
initCommands[initCommands.length - 1].output.push({
208+
type: "stdout",
209+
message: line,
210+
});
211+
}
212+
}
213+
}
214+
return initCommands;
215+
}, []);
216+
217+
const getCommandlineStr = useCallback(
218+
(filenames: string[]) => `node ${filenames[0]}`,
219+
[]
220+
);
221+
222+
return (
223+
<JavaScriptContext.Provider
224+
value={{
225+
ready,
226+
runCommand,
227+
checkSyntax,
228+
mutex: mutex.current,
229+
runFiles,
230+
interrupt,
231+
splitReplExamples,
232+
getCommandlineStr,
233+
}}
234+
>
235+
{children}
236+
</JavaScriptContext.Provider>
237+
);
238+
}

app/terminal/runtime.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ReplOutput, SyntaxStatus, ReplCommand } from "./repl";
33
import { PyodideProvider, usePyodide } from "./python/runtime";
44
import { RubyProvider, useRuby } from "./ruby/runtime";
55
import { useWandbox, WandboxProvider } from "./wandbox/runtime";
6+
import { JavaScriptProvider, useJavaScript } from "./javascript/runtime";
67
import { AceLang } from "./editor";
78
import { ReactNode } from "react";
89

@@ -29,7 +30,7 @@ export interface LangConstants {
2930
prompt?: string;
3031
promptMore?: string;
3132
}
32-
export type RuntimeLang = "python" | "ruby" | "cpp";
33+
export type RuntimeLang = "python" | "ruby" | "cpp" | "javascript";
3334

3435
export function getRuntimeLang(
3536
lang: string | undefined
@@ -45,6 +46,9 @@ export function getRuntimeLang(
4546
case "cpp":
4647
case "c++":
4748
return "cpp";
49+
case "javascript":
50+
case "js":
51+
return "javascript";
4852
default:
4953
console.warn(`Unsupported language for runtime: ${lang}`);
5054
return undefined;
@@ -55,6 +59,7 @@ export function useRuntime(language: RuntimeLang): RuntimeContext {
5559
const pyodide = usePyodide();
5660
const ruby = useRuby();
5761
const wandboxCpp = useWandbox("cpp");
62+
const javascript = useJavaScript();
5863

5964
switch (language) {
6065
case "python":
@@ -63,6 +68,8 @@ export function useRuntime(language: RuntimeLang): RuntimeContext {
6368
return ruby;
6469
case "cpp":
6570
return wandboxCpp;
71+
case "javascript":
72+
return javascript;
6673
default:
6774
language satisfies never;
6875
throw new Error(`Runtime not implemented for language: ${language}`);
@@ -72,7 +79,9 @@ export function RuntimeProvider({ children }: { children: ReactNode }) {
7279
return (
7380
<PyodideProvider>
7481
<RubyProvider>
75-
<WandboxProvider>{children}</WandboxProvider>
82+
<WandboxProvider>
83+
<JavaScriptProvider>{children}</JavaScriptProvider>
84+
</WandboxProvider>
7685
</RubyProvider>
7786
</PyodideProvider>
7887
);
@@ -92,6 +101,11 @@ export function langConstants(lang: RuntimeLang | AceLang): LangConstants {
92101
prompt: ">> ",
93102
promptMore: "?> ",
94103
};
104+
case "javascript":
105+
return {
106+
tabSize: 2,
107+
prompt: "> ",
108+
};
95109
case "c_cpp":
96110
case "cpp":
97111
return {

0 commit comments

Comments
 (0)