Skip to content

Commit 461a1b0

Browse files
committed
TypeScript実行環境を追加
* `@typescript/vfs` で型チェック&トランスパイル * typescriptで生成されたjsファイルをjsEvalのコンテキストに渡すため、writeFile()とrunFiles()の動作を変更しています * writeFile() が更新後の全ファイルをpromiseで返し、 runFiles() はcontextからファイルを取得する代わりに引数で受け取る * (runFiles() の呼び出し側は常にfilesをContextから取ってきて引数に渡さないといけなくなる)
1 parent be5b650 commit 461a1b0

File tree

14 files changed

+291
-53
lines changed

14 files changed

+291
-53
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ yarn-error.log*
4242
# typescript
4343
*.tsbuildinfo
4444
next-env.d.ts
45+
46+
47+
/public/typescript/

app/terminal/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ
5151
5252
### ファイル実行用
5353
54-
* runFiles: `(filenames: string[]) => Promise<ReplOutput[]>`
55-
* 指定されたファイルを実行します。ファイルの中身はEmbedContextから取得されます。
54+
* runFiles: `(filenames: string[], files: Record<string, string>) => Promise<ReplOutput[]>`
55+
* 指定されたファイルを実行します。
56+
* EmbedContextから取得したfilesを呼び出し側で引数に渡します
5657
* 呼び出し側でmutexのロックはせず、必要であればrunFiles()内でロックします。
5758
* getCommandlineStr: `(filenames: string[]) => string`
5859
* 指定されたファイルを実行するためのコマンドライン引数文字列を返します。表示用です。

app/terminal/editor.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function EditorComponent(props: EditorProps) {
7373
const code = files[props.filename] || props.initContent;
7474
useEffect(() => {
7575
if (!files[props.filename]) {
76-
writeFile(props.filename, props.initContent);
76+
writeFile({ [props.filename]: props.initContent });
7777
}
7878
}, [files, props.filename, props.initContent, writeFile]);
7979

@@ -92,7 +92,7 @@ export function EditorComponent(props: EditorProps) {
9292
// codeの内容が変更された場合のみ表示する
9393
(props.readonly || code == props.initContent) && "invisible"
9494
)}
95-
onClick={() => writeFile(props.filename, props.initContent)}
95+
onClick={() => writeFile({ [props.filename]: props.initContent })}
9696
>
9797
{/*<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->*/}
9898
<svg
@@ -131,7 +131,7 @@ export function EditorComponent(props: EditorProps) {
131131
enableLiveAutocompletion={true}
132132
enableSnippets={false}
133133
value={code}
134-
onChange={(code: string) => writeFile(props.filename, code)}
134+
onChange={(code: string) => writeFile({ [props.filename]: code })}
135135
/>
136136
</div>
137137
);

app/terminal/embedContext.tsx

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ type TerminalId = string;
2525

2626
interface IEmbedContext {
2727
files: Record<Filename, string>;
28-
writeFile: (name: Filename, content: string) => void;
28+
// ファイルを書き込む。更新後のページ内の全ファイル内容を返す
29+
// 返り値を使うことで再レンダリングを待たずに最新の内容を取得できる
30+
writeFile: (
31+
updatedFiles: Record<Filename, string>
32+
) => Promise<Record<Filename, string>>;
2933

3034
replOutputs: Record<TerminalId, ReplCommand[]>;
3135
addReplOutput: (
@@ -72,16 +76,26 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) {
7276
}, [pathname, currentPathname]);
7377

7478
const writeFile = useCallback(
75-
(name: Filename, content: string) => {
76-
setFiles((files) => {
77-
if (files[pathname]?.[name] !== content) {
78-
files = { ...files };
79-
files[pathname] = { ...(files[pathname] ?? {}) };
80-
files[pathname][name] = content;
81-
return files;
82-
} else {
83-
return files;
84-
}
79+
(updatedFiles: Record<Filename, string>) => {
80+
return new Promise<Record<Filename, string>>((resolve) => {
81+
setFiles((files) => {
82+
let changed = false;
83+
const newFiles = { ...files };
84+
newFiles[pathname] = { ...(newFiles[pathname] ?? {}) };
85+
for (const [name, content] of Object.entries(updatedFiles)) {
86+
if (newFiles[pathname][name] !== content) {
87+
changed = true;
88+
newFiles[pathname][name] = content;
89+
}
90+
}
91+
if (changed) {
92+
resolve(newFiles[pathname]);
93+
return newFiles;
94+
} else {
95+
resolve(files[pathname] || {});
96+
return files;
97+
}
98+
});
8599
});
86100
},
87101
[pathname]

app/terminal/exec.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function ExecFile(props: ExecProps) {
3131
}
3232
},
3333
});
34-
const { setExecResult } = useEmbedContext();
34+
const { files, setExecResult } = useEmbedContext();
3535

3636
const { ready, runFiles, getCommandlineStr } = useRuntime(props.language);
3737

@@ -45,7 +45,7 @@ export function ExecFile(props: ExecProps) {
4545
(async () => {
4646
clearTerminal(terminalInstanceRef.current!);
4747
terminalInstanceRef.current!.write(systemMessageColor("実行中です..."));
48-
const outputs = await runFiles(props.filenames);
48+
const outputs = await runFiles(props.filenames, files);
4949
clearTerminal(terminalInstanceRef.current!);
5050
writeOutput(terminalInstanceRef.current!, outputs, false);
5151
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
@@ -60,6 +60,7 @@ export function ExecFile(props: ExecProps) {
6060
runFiles,
6161
setExecResult,
6262
terminalInstanceRef,
63+
files,
6364
]);
6465

6566
return (

app/terminal/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ const sampleConfig: Record<RuntimeLang, SampleConfig> = {
7474
},
7575
exec: ["main.js"],
7676
},
77+
typescript: {
78+
repl: false,
79+
editor: {
80+
"main.ts": 'function greet(name: string): void {\n console.log("Hello, " + name + "!");\n}\n\ngreet("World");',
81+
},
82+
exec: ["main.ts"],
83+
},
7784
cpp: {
7885
repl: false,
7986
editor: {

app/terminal/runtime.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { PyodideContext, usePyodide } from "./worker/pyodide";
77
import { RubyContext, useRuby } from "./worker/ruby";
88
import { JSEvalContext, useJSEval } from "./worker/jsEval";
99
import { WorkerProvider } from "./worker/runtime";
10+
import { TypeScriptProvider, useTypeScript } from "./typescript/runtime";
1011

1112
/**
1213
* Common runtime context interface for different languages
@@ -23,15 +24,20 @@ export interface RuntimeContext {
2324
checkSyntax?: (code: string) => Promise<SyntaxStatus>;
2425
splitReplExamples?: (content: string) => ReplCommand[];
2526
// file
26-
runFiles: (filenames: string[]) => Promise<ReplOutput[]>;
27+
runFiles: (filenames: string[], files: Record<string, string>) => Promise<ReplOutput[]>;
2728
getCommandlineStr?: (filenames: string[]) => string;
2829
}
2930
export interface LangConstants {
3031
tabSize: number;
3132
prompt?: string;
3233
promptMore?: string;
3334
}
34-
export type RuntimeLang = "python" | "ruby" | "cpp" | "javascript";
35+
export type RuntimeLang =
36+
| "python"
37+
| "ruby"
38+
| "cpp"
39+
| "javascript"
40+
| "typescript";
3541

3642
export function getRuntimeLang(
3743
lang: string | undefined
@@ -50,6 +56,9 @@ export function getRuntimeLang(
5056
case "javascript":
5157
case "js":
5258
return "javascript";
59+
case "typescript":
60+
case "ts":
61+
return "typescript";
5362
default:
5463
console.warn(`Unsupported language for runtime: ${lang}`);
5564
return undefined;
@@ -60,6 +69,7 @@ export function useRuntime(language: RuntimeLang): RuntimeContext {
6069
const pyodide = usePyodide();
6170
const ruby = useRuby();
6271
const jsEval = useJSEval();
72+
const typescript = useTypeScript(jsEval);
6373
const wandboxCpp = useWandbox("cpp");
6474

6575
switch (language) {
@@ -69,6 +79,8 @@ export function useRuntime(language: RuntimeLang): RuntimeContext {
6979
return ruby;
7080
case "javascript":
7181
return jsEval;
82+
case "typescript":
83+
return typescript;
7284
case "cpp":
7385
return wandboxCpp;
7486
default:
@@ -81,7 +93,9 @@ export function RuntimeProvider({ children }: { children: ReactNode }) {
8193
<WorkerProvider context={PyodideContext} script="/pyodide.worker.js">
8294
<WorkerProvider context={RubyContext} script="/ruby.worker.js">
8395
<WorkerProvider context={JSEvalContext} script="/javascript.worker.js">
84-
<WandboxProvider>{children}</WandboxProvider>
96+
<WandboxProvider>
97+
<TypeScriptProvider>{children}</TypeScriptProvider>
98+
</WandboxProvider>
8599
</WorkerProvider>
86100
</WorkerProvider>
87101
</WorkerProvider>
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"use client";
2+
3+
import ts, { CompilerOptions } from "typescript";
4+
import {
5+
createSystem,
6+
createVirtualTypeScriptEnvironment,
7+
knownLibFilesForCompilerOptions,
8+
VirtualTypeScriptEnvironment,
9+
} from "@typescript/vfs";
10+
import {
11+
createContext,
12+
ReactNode,
13+
useCallback,
14+
useContext,
15+
useEffect,
16+
useRef,
17+
useState,
18+
} from "react";
19+
import { useEmbedContext } from "../embedContext";
20+
import { ReplOutput } from "../repl";
21+
import { RuntimeContext } from "../runtime";
22+
23+
export const compilerOptions: CompilerOptions = {};
24+
25+
const TypeScriptContext = createContext<VirtualTypeScriptEnvironment | null>(
26+
null
27+
);
28+
export function TypeScriptProvider({ children }: { children: ReactNode }) {
29+
const [tsEnv, setTSEnv] = useState<VirtualTypeScriptEnvironment | null>(null);
30+
useEffect(() => {
31+
if (tsEnv === null) {
32+
const abortController = new AbortController();
33+
(async () => {
34+
const system = createSystem(new Map());
35+
const libFiles = knownLibFilesForCompilerOptions(compilerOptions, ts);
36+
const libFileContents = await Promise.all(
37+
libFiles.map(async (libFile) => {
38+
const response = await fetch(
39+
`/typescript/${ts.version}/${libFile}`,
40+
{ signal: abortController.signal }
41+
);
42+
if (response.ok) {
43+
return response.text();
44+
} else {
45+
return undefined;
46+
}
47+
})
48+
);
49+
libFiles.forEach((libFile, index) => {
50+
const content = libFileContents[index];
51+
if (content !== undefined) {
52+
system.writeFile(`/${libFile}`, content);
53+
}
54+
});
55+
const env = createVirtualTypeScriptEnvironment(
56+
system,
57+
[],
58+
ts,
59+
compilerOptions
60+
);
61+
setTSEnv(env);
62+
})();
63+
return () => {
64+
abortController.abort();
65+
};
66+
}
67+
}, [tsEnv]);
68+
return (
69+
<TypeScriptContext.Provider value={tsEnv}>
70+
{children}
71+
</TypeScriptContext.Provider>
72+
);
73+
}
74+
75+
export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {
76+
const tsEnv = useContext(TypeScriptContext);
77+
78+
const { writeFile } = useEmbedContext();
79+
const runFiles = useCallback(
80+
async (filenames: string[], files: Record<string, string>) => {
81+
if (tsEnv === null) {
82+
return [
83+
{ type: "error" as const, message: "TypeScript is not ready yet." },
84+
];
85+
}
86+
87+
for (const [filename, content] of Object.entries(files)) {
88+
tsEnv.createFile(filename, content);
89+
}
90+
91+
const outputs: ReplOutput[] = [];
92+
93+
for (const diagnostic of tsEnv.languageService.getSyntacticDiagnostics(
94+
filenames[0]
95+
)) {
96+
outputs.push({
97+
type: "error",
98+
message: ts.formatDiagnosticsWithColorAndContext([diagnostic], {
99+
getCurrentDirectory: () => "",
100+
getCanonicalFileName: (f) => f,
101+
getNewLine: () => "\n",
102+
}),
103+
});
104+
}
105+
106+
for (const diagnostic of tsEnv.languageService.getSemanticDiagnostics(
107+
filenames[0]
108+
)) {
109+
outputs.push({
110+
type: "error",
111+
message: ts.formatDiagnosticsWithColorAndContext([diagnostic], {
112+
getCurrentDirectory: () => "",
113+
getCanonicalFileName: (f) => f,
114+
getNewLine: () => "\n",
115+
}),
116+
});
117+
}
118+
119+
const emitOutput = tsEnv.languageService.getEmitOutput(filenames[0]);
120+
files = await writeFile(Object.fromEntries(emitOutput.outputFiles.map((of) => [of.name, of.text])));
121+
122+
console.log(emitOutput)
123+
const jsOutputs = jsEval.runFiles([emitOutput.outputFiles[0].name], files);
124+
125+
return outputs.concat(await jsOutputs);
126+
},
127+
[tsEnv, writeFile, jsEval]
128+
);
129+
return {
130+
ready: tsEnv !== null,
131+
runFiles,
132+
getCommandlineStr,
133+
};
134+
}
135+
136+
function getCommandlineStr(filenames: string[]) {
137+
return `tsc ${filenames.join(" ")}`;
138+
}

0 commit comments

Comments
 (0)