Skip to content

Commit d2c40aa

Browse files
authored
Merge pull request #96 from ut-code/typescript
TypeScript実行環境を追加
2 parents 87a41d7 + 6d20622 commit d2c40aa

24 files changed

+634
-247
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/[docs_id]/markdown.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Markdown, { Components } from "react-markdown";
22
import remarkGfm from "remark-gfm";
3-
import SyntaxHighlighter from "react-syntax-highlighter";
43
import { EditorComponent, getAceLang } from "../terminal/editor";
54
import { ExecFile } from "../terminal/exec";
65
import { useChangeTheme } from "./themeToggle";
@@ -11,6 +10,9 @@ import {
1110
import { ReactNode } from "react";
1211
import { getRuntimeLang } from "@/terminal/runtime";
1312
import { ReplTerminal } from "@/terminal/repl";
13+
import dynamic from "next/dynamic";
14+
// SyntaxHighlighterはファイルサイズがでかいので & HydrationErrorを起こすので、SSRを無効化する
15+
const SyntaxHighlighter = dynamic(() => import("react-syntax-highlighter"), { ssr: false });
1416

1517
export function StyledMarkdown({ content }: { content: string }) {
1618
return (

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: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const AceEditor = dynamic(
1414
await import("ace-builds/src-min-noconflict/mode-ruby");
1515
await import("ace-builds/src-min-noconflict/mode-c_cpp");
1616
await import("ace-builds/src-min-noconflict/mode-javascript");
17+
await import("ace-builds/src-min-noconflict/mode-typescript");
1718
await import("ace-builds/src-min-noconflict/mode-json");
1819
await import("ace-builds/src-min-noconflict/mode-csv");
1920
await import("ace-builds/src-min-noconflict/mode-text");
@@ -30,7 +31,15 @@ import { langConstants } from "./runtime";
3031
// snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python";
3132

3233
// mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値が対応する
33-
export type AceLang = "python" | "ruby" | "c_cpp" | "javascript" | "json" | "csv" | "text";
34+
export type AceLang =
35+
| "python"
36+
| "ruby"
37+
| "c_cpp"
38+
| "javascript"
39+
| "typescript"
40+
| "json"
41+
| "csv"
42+
| "text";
3443
export function getAceLang(lang: string | undefined): AceLang {
3544
// Markdownで指定される可能性のある言語名からAceLangを取得
3645
switch (lang) {
@@ -46,6 +55,9 @@ export function getAceLang(lang: string | undefined): AceLang {
4655
case "javascript":
4756
case "js":
4857
return "javascript";
58+
case "typescript":
59+
case "ts":
60+
return "typescript";
4961
case "json":
5062
return "json";
5163
case "csv":
@@ -73,7 +85,7 @@ export function EditorComponent(props: EditorProps) {
7385
const code = files[props.filename] || props.initContent;
7486
useEffect(() => {
7587
if (!files[props.filename]) {
76-
writeFile(props.filename, props.initContent);
88+
writeFile({ [props.filename]: props.initContent });
7789
}
7890
}, [files, props.filename, props.initContent, writeFile]);
7991

@@ -92,7 +104,7 @@ export function EditorComponent(props: EditorProps) {
92104
// codeの内容が変更された場合のみ表示する
93105
(props.readonly || code == props.initContent) && "invisible"
94106
)}
95-
onClick={() => writeFile(props.filename, props.initContent)}
107+
onClick={() => writeFile({ [props.filename]: props.initContent })}
96108
>
97109
{/*<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->*/}
98110
<svg
@@ -131,7 +143,7 @@ export function EditorComponent(props: EditorProps) {
131143
enableLiveAutocompletion={true}
132144
enableSnippets={false}
133145
value={code}
134-
onChange={(code: string) => writeFile(props.filename, code)}
146+
onChange={(code: string) => writeFile({ [props.filename]: code })}
135147
/>
136148
</div>
137149
);

app/terminal/embedContext.tsx

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,21 @@ type Filename = string;
2424
type TerminalId = string;
2525

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

30-
replOutputs: Record<TerminalId, ReplCommand[]>;
34+
replOutputs: Readonly<Record<TerminalId, ReplCommand[]>>;
3135
addReplOutput: (
3236
terminalId: TerminalId,
3337
command: string,
3438
output: ReplOutput[]
3539
) => void;
3640

37-
execResults: Record<Filename, ReplOutput[]>;
41+
execResults: Readonly<Record<Filename, ReplOutput[]>>;
3842
setExecResult: (filename: Filename, output: ReplOutput[]) => void;
3943
}
4044
const EmbedContext = createContext<IEmbedContext>(null!);
@@ -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: 4 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,13 +45,14 @@ 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(
5151
terminalInstanceRef.current!,
5252
outputs,
5353
false,
5454
undefined,
55+
null, // ファイル実行で"return"メッセージが返ってくることはないはずなので、Prismを渡す必要はない
5556
props.language
5657
);
5758
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
@@ -67,6 +68,7 @@ export function ExecFile(props: ExecProps) {
6768
setExecResult,
6869
terminalInstanceRef,
6970
props.language,
71+
files,
7072
]);
7173

7274
return (

app/terminal/highlight.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
import Prism from "prismjs";
21
import chalk from "chalk";
32
import { RuntimeLang } from "./runtime";
4-
// 言語定義をインポート
5-
import "prismjs/components/prism-python";
6-
import "prismjs/components/prism-ruby";
7-
import "prismjs/components/prism-javascript";
3+
4+
export async function importPrism() {
5+
if (typeof window !== "undefined") {
6+
const Prism = await import("prismjs");
7+
// 言語定義をインポート
8+
await import("prismjs/components/prism-python");
9+
await import("prismjs/components/prism-ruby");
10+
await import("prismjs/components/prism-javascript");
11+
return Prism;
12+
} else {
13+
return null!;
14+
}
15+
}
816

917
type PrismLang = "python" | "ruby" | "javascript";
1018

@@ -17,6 +25,7 @@ function getPrismLanguage(language: RuntimeLang): PrismLang {
1725
case "javascript":
1826
return "javascript";
1927
case "cpp":
28+
case "typescript":
2029
throw new Error(
2130
`highlight for ${language} is disabled because it should not support REPL`
2231
);
@@ -74,6 +83,7 @@ const prismToAnsi: Record<string, (text: string) => string> = {
7483
* @returns {string} ANSIで色付けされた文字列
7584
*/
7685
export function highlightCodeToAnsi(
86+
Prism: typeof import("prismjs"),
7787
code: string,
7888
language: RuntimeLang
7989
): string {
@@ -129,3 +139,4 @@ export function highlightCodeToAnsi(
129139
""
130140
);
131141
}
142+

app/terminal/page.tsx

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"use client";
22
import { Heading } from "@/[docs_id]/markdown";
3-
import "mocha/mocha.js";
43
import "mocha/mocha.css";
54
import { Fragment, useEffect, useRef, useState } from "react";
65
import { useWandbox } from "./wandbox/runtime";
@@ -13,6 +12,7 @@ import { useJSEval } from "./worker/jsEval";
1312
import { ReplTerminal } from "./repl";
1413
import { EditorComponent, getAceLang } from "./editor";
1514
import { ExecFile } from "./exec";
15+
import { useTypeScript } from "./typescript/runtime";
1616

1717
export default function RuntimeTestPage() {
1818
return (
@@ -69,8 +69,17 @@ const sampleConfig: Record<RuntimeLang, SampleConfig> = {
6969
javascript: {
7070
repl: true,
7171
replInitContent: '> console.log("Hello, World!");\nHello, World!',
72-
editor: false,
73-
exec: false,
72+
editor: {
73+
"main.js": 'console.log("Hello, World!");',
74+
},
75+
exec: ["main.js"],
76+
},
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"],
7483
},
7584
cpp: {
7685
repl: false,
@@ -122,13 +131,15 @@ function RuntimeSample({
122131
function MochaTest() {
123132
const pyodide = usePyodide();
124133
const ruby = useRuby();
125-
const javascript = useJSEval();
134+
const jsEval = useJSEval();
135+
const typescript = useTypeScript(jsEval);
126136
const wandboxCpp = useWandbox("cpp");
127137
const runtimeRef = useRef<Record<RuntimeLang, RuntimeContext>>(null!);
128138
runtimeRef.current = {
129139
python: pyodide,
130140
ruby: ruby,
131-
javascript: javascript,
141+
javascript: jsEval,
142+
typescript: typescript,
132143
cpp: wandboxCpp,
133144
};
134145

@@ -139,21 +150,27 @@ function MochaTest() {
139150
const [mochaState, setMochaState] = useState<"idle" | "running" | "finished">(
140151
"idle"
141152
);
142-
const { writeFile } = useEmbedContext();
153+
const { files } = useEmbedContext();
154+
const filesRef = useRef(files);
155+
filesRef.current = files;
143156

144-
const runTest = () => {
145-
setMochaState("running");
157+
const runTest = async () => {
158+
if(typeof window !== "undefined") {
159+
setMochaState("running");
160+
161+
await import("mocha/mocha.js");
146162

147-
mocha.setup("bdd");
163+
mocha.setup("bdd");
148164

149-
for (const lang of Object.keys(runtimeRef.current) as RuntimeLang[]) {
150-
defineTests(lang, runtimeRef, writeFile);
151-
}
165+
for (const lang of Object.keys(runtimeRef.current) as RuntimeLang[]) {
166+
defineTests(lang, runtimeRef, filesRef);
167+
}
152168

153-
const runner = mocha.run();
154-
runner.on("end", () => {
155-
setMochaState("finished");
156-
});
169+
const runner = mocha.run();
170+
runner.on("end", () => {
171+
setMochaState("finished");
172+
});
173+
}
157174
};
158175

159176
return (

0 commit comments

Comments
 (0)