diff --git a/Dockerfile b/Dockerfile index 6d5eb348..81385b4b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ WORKDIR /app COPY package.json package-lock.json ./ COPY packages/jsEval/package.json ./packages/jsEval/ +COPY packages/runtime/package.json ./packages/runtime/ RUN --mount=type=cache,target=/root/.npm \ npm ci --no-audit --no-fund diff --git a/README.md b/README.md index ad4d4603..a3ea961f 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,8 @@ Cloudflare Worker のビルドログとステータス表示が見れますが ## markdown仕様 +実行環境の説明は ./packages/runtime/README.md を参照 + ```` ```言語名-repl >>> コマンド @@ -162,9 +164,9 @@ Cloudflare Worker のビルドログとステータス表示が見れますが ```` でターミナルを埋め込む。 -* ターミナル表示部は app/terminal/terminal.tsx -* コマンド入力処理は app/terminal/repl.tsx -* 各言語の実行環境は app/terminal/言語名/ 内に書く。 +* ターミナル表示部は ./packages/runtime/terminal.tsx +* コマンド入力処理は ./packages/runtime/repl.tsx +* 各言語の実行環境は ./packages/runtime/言語名/ 内に書く。 * 実行結果はSectionContextにも送られ、section.tsxからアクセスできる ```` @@ -174,10 +176,10 @@ Cloudflare Worker のビルドログとステータス表示が見れますが ```` でテキストエディターを埋め込む。 -* app/terminal/editor.tsx +* ./packages/runtime/editor.tsx * editor.tsx内で `import "ace-builds/src-min-noconflict/mode-言語名";` を追加すればその言語に対応した色付けがされる。 * importできる言語の一覧は https://github.com/ajaxorg/ace-builds/tree/master/src-noconflict -* 編集した内容は app/terminal/file.tsx のFileContextで管理される。 +* 編集した内容は ./packages/runtime/file.tsx のFileContextで管理される。 * 編集中のコードはFileContextに即時送られる * FileContextが書き換えられたら即時すべてのエディターに反映される * 編集したファイルの一覧はSectionContextにも送られ、section.tsxからアクセスできる @@ -198,7 +200,7 @@ Cloudflare Worker のビルドログとステータス表示が見れますが で実行ボタンを表示する * 実行ボタンを押した際にFileContextからファイルを読み、実行し、結果を表示する -* app/terminal/exec.tsx に各言語ごとの実装を書く (それぞれ app/terminal/言語名/ 内に定義した関数を呼び出す) +* ./packages/runtime/exec.tsx に各言語ごとの実装を書く (それぞれ ./packages/runtime/言語名/ 内に定義した関数を呼び出す) * 実行結果はSectionContextにも送られ、section.tsxからアクセスできる diff --git a/app/[lang]/[pageId]/chatForm.tsx b/app/[lang]/[pageId]/chatForm.tsx index 85d81388..ec5b051e 100644 --- a/app/[lang]/[pageId]/chatForm.tsx +++ b/app/[lang]/[pageId]/chatForm.tsx @@ -8,7 +8,7 @@ import { useState, FormEvent, useEffect } from "react"; // } from "../actions/questionExample"; // import { getLanguageName } from "../pagesList"; import { DynamicMarkdownSection } from "./pageContent"; -import { useEmbedContext } from "@/terminal/embedContext"; +import { useEmbedContext } from "@my-code/runtime/embedContext"; import { useChatHistoryContext } from "./chatHistory"; import { askAI } from "@/actions/chatActions"; diff --git a/app/[lang]/[pageId]/markdown.tsx b/app/[lang]/[pageId]/markdown.tsx index 05b446c2..930a2ccf 100644 --- a/app/[lang]/[pageId]/markdown.tsx +++ b/app/[lang]/[pageId]/markdown.tsx @@ -2,11 +2,11 @@ import Markdown, { Components, ExtraProps } from "react-markdown"; import remarkGfm from "remark-gfm"; import removeComments from "remark-remove-comments"; import remarkCjkFriendly from "remark-cjk-friendly"; -import { EditorComponent, getAceLang } from "@/terminal/editor"; -import { ExecFile } from "@/terminal/exec"; +import { EditorComponent, getAceLang } from "@my-code/runtime/editor"; +import { ExecFile } from "@my-code/runtime/exec"; import { JSX, ReactNode } from "react"; -import { getRuntimeLang } from "@/terminal/runtime"; -import { ReplTerminal } from "@/terminal/repl"; +import { getRuntimeLang } from "@my-code/runtime/languages"; +import { ReplTerminal } from "@my-code/runtime/repl"; import { getSyntaxHighlighterLang, MarkdownLang, diff --git a/app/actions/chatActions.ts b/app/actions/chatActions.ts index dfbb7180..92811221 100644 --- a/app/actions/chatActions.ts +++ b/app/actions/chatActions.ts @@ -3,7 +3,7 @@ // import { z } from "zod"; import { generateContent } from "./gemini"; import { DynamicMarkdownSection } from "../[lang]/[pageId]/pageContent"; -import { ReplCommand, ReplOutput } from "../terminal/repl"; +import { ReplCommand, ReplOutput } from "@my-code/runtime/repl"; import { addChat, ChatWithMessages } from "@/lib/chatHistory"; type ChatResult = diff --git a/app/layout.tsx b/app/layout.tsx index 8139b106..203419a3 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,10 +8,10 @@ import "./globals.css"; import { Navbar } from "./navbar"; import { Sidebar } from "./sidebar"; import { ReactNode } from "react"; -import { EmbedContextProvider } from "./terminal/embedContext"; +import { EmbedContextProvider } from "@my-code/runtime/embedContext"; import { AutoAnonymousLogin } from "./accountMenu"; import { SidebarMdProvider } from "./sidebar"; -import { RuntimeProvider } from "./terminal/runtime"; +import { RuntimeProvider } from "@my-code/runtime/context"; import { getPagesList } from "@/lib/docs"; export const metadata: Metadata = { diff --git a/app/sidebar.tsx b/app/sidebar.tsx index 6aead65e..8a30ecaa 100644 --- a/app/sidebar.tsx +++ b/app/sidebar.tsx @@ -14,8 +14,8 @@ import { } from "react"; import { DynamicMarkdownSection } from "./[lang]/[pageId]/pageContent"; import clsx from "clsx"; -import { LanguageIcon } from "./terminal/icons"; -import { RuntimeLang } from "./terminal/runtime"; +import { LanguageIcon } from "@my-code/runtime/icons"; +import { RuntimeLang } from "@my-code/runtime/languages"; export interface ISidebarMdContext { loadedDocsId: { lang: string; pageId: string } | null; diff --git a/app/terminal/page.tsx b/app/terminal/page.tsx index a5dcafb3..753e8dc3 100644 --- a/app/terminal/page.tsx +++ b/app/terminal/page.tsx @@ -3,28 +3,29 @@ import { Heading } from "@/[lang]/[pageId]/markdown"; import "mocha/mocha.css"; import { Fragment, useEffect, useRef, useState } from "react"; -import { useWandbox } from "./wandbox/runtime"; -import { RuntimeContext, RuntimeLang } from "./runtime"; -import { useEmbedContext } from "./embedContext"; -import { defineTests } from "./tests"; -import { usePyodide } from "./worker/pyodide"; -import { useRuby } from "./worker/ruby"; -import { useJSEval } from "./worker/jsEval"; -import { ReplTerminal } from "./repl"; -import { EditorComponent, getAceLang } from "./editor"; -import { ExecFile } from "./exec"; -import { useTypeScript } from "./typescript/runtime"; -import { useTerminal } from "./terminal"; +import { useWandbox } from "@my-code/runtime/wandbox/runtime"; +import { RuntimeContext } from "@my-code/runtime/interface"; +import { RuntimeLang } from "@my-code/runtime/languages"; +import { useEmbedContext } from "@my-code/runtime/embedContext"; +import { defineTests } from "@my-code/runtime/tests"; +import { usePyodide } from "@my-code/runtime/worker/pyodide"; +import { useRuby } from "@my-code/runtime/worker/ruby"; +import { useJSEval } from "@my-code/runtime/worker/jsEval"; +import { ReplTerminal } from "@my-code/runtime/repl"; +import { EditorComponent, getAceLang } from "@my-code/runtime/editor"; +import { ExecFile } from "@my-code/runtime/exec"; +import { useTypeScript } from "@my-code/runtime/typescript/runtime"; +import { useTerminal } from "@my-code/runtime/terminal"; -import main_py from "./samples/main.py?raw"; -import main_rb from "./samples/main.rb?raw"; -import main_js from "./samples/main.js?raw"; -import main2_ts from "./samples/main2.ts?raw"; -import main_cpp from "./samples/main.cpp?raw"; -import sub_h from "./samples/sub.h?raw"; -import sub_cpp from "./samples/sub.cpp?raw"; -import main2_rs from "./samples/main2.rs?raw"; -import sub_rs from "./samples/sub.rs?raw"; +import main_py from "@my-code/runtime/samples/main.py?raw"; +import main_rb from "@my-code/runtime/samples/main.rb?raw"; +import main_js from "@my-code/runtime/samples/main.js?raw"; +import main2_ts from "@my-code/runtime/samples/main2.ts?raw"; +import main_cpp from "@my-code/runtime/samples/main.cpp?raw"; +import sub_h from "@my-code/runtime/samples/sub.h?raw"; +import sub_cpp from "@my-code/runtime/samples/sub.cpp?raw"; +import main2_rs from "@my-code/runtime/samples/main2.rs?raw"; +import sub_rs from "@my-code/runtime/samples/sub.rs?raw"; export default function RuntimeTestPage() { return ( diff --git a/app/terminal/runtime.tsx b/app/terminal/runtime.tsx deleted file mode 100644 index 5fe2e75b..00000000 --- a/app/terminal/runtime.tsx +++ /dev/null @@ -1,218 +0,0 @@ -"use client"; - -import { MutexInterface } from "async-mutex"; -import { ReplOutput, SyntaxStatus, ReplCommand } from "./repl"; -import { useWandbox, WandboxProvider } from "./wandbox/runtime"; -import { AceLang } from "./editor"; -import { ReactNode, useEffect } from "react"; -import { PyodideContext, usePyodide } from "./worker/pyodide"; -import { RubyContext, useRuby } from "./worker/ruby"; -import { JSEvalContext, useJSEval } from "./worker/jsEval"; -import { WorkerProvider } from "./worker/runtime"; -import { TypeScriptProvider, useTypeScript } from "./typescript/runtime"; -import { MarkdownLang } from "@/[lang]/[pageId]/styledSyntaxHighlighter"; - -/** - * Common runtime context interface for different languages - * - * see README.md for details - * - */ -export interface RuntimeContext { - init?: () => void; - ready: boolean; - mutex?: MutexInterface; - interrupt?: () => void; - // repl - runCommand?: ( - command: string, - onOutput: (output: ReplOutput) => void - ) => Promise; - checkSyntax?: (code: string) => Promise; - splitReplExamples?: (content: string) => ReplCommand[]; - // file - runFiles: ( - filenames: string[], - files: Readonly>, - onOutput: (output: ReplOutput) => void - ) => Promise; - getCommandlineStr?: (filenames: string[]) => string; - runtimeInfo?: RuntimeInfo; -} -export interface RuntimeInfo { - prettyLangName: string; - version?: string; -} -export interface LangConstants { - tabSize: number; - prompt?: string; - promptMore?: string; - returnPrefix?: string; -} -export type RuntimeLang = - | "python" - | "ruby" - | "cpp" - | "rust" - | "javascript" - | "typescript"; - -export function getRuntimeLang( - lang: MarkdownLang | undefined -): RuntimeLang | undefined { - // markdownで指定される可能性のある言語名からRuntimeLangを取得 - switch (lang) { - case "python": - case "py": - return "python"; - case "ruby": - case "rb": - return "ruby"; - case "cpp": - case "c++": - return "cpp"; - case "rust": - case "rs": - return "rust"; - case "javascript": - case "js": - return "javascript"; - case "typescript": - case "ts": - return "typescript"; - case "bash": - case "sh": - case "powershell": - case "json": - case "toml": - case "csv": - case "text": - case "txt": - case "html": - case "makefile": - case "cmake": - case undefined: - // unsupported languages - return undefined; - default: - lang satisfies never; - console.error(`getRuntimeLang() does not handle language ${lang}`); - return undefined; - } -} -export function useRuntime(language: RuntimeLang): RuntimeContext { - // すべての言語のcontextをインスタンス化 - const pyodide = usePyodide(); - const ruby = useRuby(); - const jsEval = useJSEval(); - const typescript = useTypeScript(jsEval); - const wandboxCpp = useWandbox("cpp"); - const wandboxRust = useWandbox("rust"); - - let runtime: RuntimeContext; - switch (language) { - case "python": - runtime = pyodide; - break; - case "ruby": - runtime = ruby; - break; - case "javascript": - runtime = jsEval; - break; - case "typescript": - runtime = typescript; - break; - case "cpp": - runtime = wandboxCpp; - break; - case "rust": - runtime = wandboxRust; - break; - default: - language satisfies never; - throw new Error(`Runtime not implemented for language: ${language}`); - } - const { init } = runtime; - useEffect(() => { - init?.(); - }, [init]); - return runtime; -} -export function RuntimeProvider({ children }: { children: ReactNode }) { - return ( - - - - - {children} - - - - - ); -} - -export function langConstants(lang: RuntimeLang | AceLang): LangConstants { - switch (lang) { - case "python": - return { - tabSize: 4, - prompt: ">>> ", - promptMore: "... ", - }; - case "ruby": - return { - tabSize: 2, - // TODO: 実際のirbのプロンプトは静的でなく、(main)や番号などの動的な表示がある - prompt: "irb> ", - promptMore: "irb* ", - returnPrefix: "=> ", - }; - case "javascript": - case "typescript": - return { - tabSize: 2, - prompt: "> ", - promptMore: "... ", - }; - case "c_cpp": - case "cpp": - return { - // 2文字派と4文字派があるが、geminiが4文字で出力するので4でいいや - tabSize: 4, - }; - case "rust": - return { - tabSize: 4, - }; - case "json": - return { - // python-7章で使っている - tabSize: 4, - }; - case "csv": - case "text": - return { - // tabは使わないが、0は指定できないようなので適当にデフォルト値 - tabSize: 4, - }; - default: - lang satisfies never; - throw new Error(`LangConstants not defined for language: ${lang}`); - } -} - -export const emptyMutex: MutexInterface = { - async runExclusive(fn: () => Promise | T) { - const result = fn(); - return result instanceof Promise ? result : Promise.resolve(result); - }, - acquire: async () => { - return () => {}; // Release function (no-op) - }, - waitForUnlock: async () => {}, - isLocked: () => false, - cancel: () => {}, - release: () => {}, -}; diff --git a/package-lock.json b/package-lock.json index 53b95448..71b27ce9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,52 +15,31 @@ "@fontsource/m-plus-rounded-1c": "^5.2.9", "@google/genai": "^1.21.0", "@opennextjs/cloudflare": "^1.16.1", - "@ruby/wasm-wasi": "^2.7.2", - "@typescript/vfs": "^1.6.2", - "@xterm/addon-fit": "0.11.0-beta.115", - "@xterm/xterm": "5.6.0-beta.115", - "ace-builds": "^1.43.2", - "async-mutex": "^0.5.0", "better-auth": "^1.4.5", - "chai": "^6.2.0", - "chalk": "^5.5.0", "clsx": "^2.1.1", - "comlink": "^4.4.2", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", "js-yaml": "^4.1.1", - "mocha": "^11.7.4", "next": "^15.5.11", - "object-inspect": "^1.13.4", "pg": "^8.16.3", - "prismjs": "^1.30.0", - "pyodide": "^0.29.0", "react": "^19", - "react-ace": "^14.0.1", "react-dom": "^19", "react-markdown": "^10.1.0", - "react-syntax-highlighter": "^16.1.0", "remark-cjk-friendly": "^1.2.3", "remark-gfm": "^4.0.1", "remark-remove-comments": "^1.1.1", "swr": "^2.3.6", - "typescript": "5.9.3", "zod": "^4.0.17" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", - "@types/chai": "^5.2.3", "@types/js-yaml": "^4.0.9", - "@types/mocha": "^10.0.10", "@types/node": "^20", - "@types/object-inspect": "^1.13.0", "@types/pako": "^2.0.4", "@types/pg": "^8.15.5", - "@types/prismjs": "^1.26.5", "@types/react": "^19", "@types/react-dom": "^19", - "@types/react-syntax-highlighter": "^15.5.13", "daisyui": "^5.5.5", "drizzle-kit": "^0.31.5", "eslint": "^9", @@ -71,6 +50,7 @@ "prisma": "^6.18.0", "tailwindcss": "^4", "tsx": "^4.20.6", + "typescript": "5.9.3", "wrangler": "^4.27.0" } }, @@ -3480,6 +3460,10 @@ "resolved": "packages/jsEval", "link": true }, + "node_modules/@my-code/runtime": { + "resolved": "packages/runtime", + "link": true + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -15602,6 +15586,38 @@ "tsx": "^4", "typescript": "^5" } + }, + "packages/runtime": { + "name": "@my-code/runtime", + "dependencies": { + "@ruby/wasm-wasi": "^2.7.2", + "@typescript/vfs": "^1.6.2", + "@xterm/addon-fit": "0.11.0-beta.115", + "@xterm/xterm": "5.6.0-beta.115", + "ace-builds": "^1.43.2", + "async-mutex": "^0.5.0", + "chai": "^6.2.0", + "chalk": "^5.5.0", + "comlink": "^4.4.2", + "mocha": "^11.7.4", + "object-inspect": "^1.13.4", + "prismjs": "^1.30.0", + "pyodide": "^0.29.0", + "react": "^19", + "react-ace": "^14.0.1", + "react-dom": "^19", + "react-syntax-highlighter": "^16.1.0", + "typescript": "^5" + }, + "devDependencies": { + "@types/chai": "^5.2.3", + "@types/mocha": "^10.0.10", + "@types/object-inspect": "^1.13.0", + "@types/prismjs": "^1.26.5", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/react-syntax-highlighter": "^15.5.13" + } } } } diff --git a/package.json b/package.json index 963fbb91..ec70619a 100644 --- a/package.json +++ b/package.json @@ -26,52 +26,31 @@ "@fontsource/m-plus-rounded-1c": "^5.2.9", "@google/genai": "^1.21.0", "@opennextjs/cloudflare": "^1.16.1", - "@ruby/wasm-wasi": "^2.7.2", - "@typescript/vfs": "^1.6.2", - "@xterm/addon-fit": "0.11.0-beta.115", - "@xterm/xterm": "5.6.0-beta.115", - "ace-builds": "^1.43.2", - "async-mutex": "^0.5.0", "better-auth": "^1.4.5", - "chai": "^6.2.0", - "chalk": "^5.5.0", "clsx": "^2.1.1", - "comlink": "^4.4.2", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", "js-yaml": "^4.1.1", - "mocha": "^11.7.4", "next": "^15.5.11", - "object-inspect": "^1.13.4", "pg": "^8.16.3", - "prismjs": "^1.30.0", - "pyodide": "^0.29.0", "react": "^19", - "react-ace": "^14.0.1", "react-dom": "^19", "react-markdown": "^10.1.0", - "react-syntax-highlighter": "^16.1.0", "remark-cjk-friendly": "^1.2.3", "remark-gfm": "^4.0.1", "remark-remove-comments": "^1.1.1", "swr": "^2.3.6", - "typescript": "5.9.3", "zod": "^4.0.17" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", "@types/js-yaml": "^4.0.9", - "@types/chai": "^5.2.3", - "@types/mocha": "^10.0.10", "@types/node": "^20", - "@types/object-inspect": "^1.13.0", "@types/pako": "^2.0.4", "@types/pg": "^8.15.5", - "@types/prismjs": "^1.26.5", "@types/react": "^19", "@types/react-dom": "^19", - "@types/react-syntax-highlighter": "^15.5.13", "daisyui": "^5.5.5", "drizzle-kit": "^0.31.5", "eslint": "^9", @@ -82,6 +61,7 @@ "prisma": "^6.18.0", "tailwindcss": "^4", "tsx": "^4.20.6", + "typescript": "5.9.3", "wrangler": "^4.27.0" } } diff --git a/app/terminal/README.md b/packages/runtime/README.md similarity index 98% rename from app/terminal/README.md rename to packages/runtime/README.md index 1d9edef5..fd89e945 100644 --- a/app/terminal/README.md +++ b/packages/runtime/README.md @@ -1,10 +1,11 @@ # my.code(); Runtime API -## runtime.tsx + +## RuntimeContext (interface.ts) 各言語のランタイムはRuntimeContextインターフェースの実装を返すフックを実装する必要があります。 -runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、その中で指定された言語のランタイムを返します。 +context.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、その中で指定された言語のランタイムを返します。 関数はすべてuseCallbackやuseMemoなどを用いレンダリングごとに同じインスタンスを返すように実装してください。 @@ -63,6 +64,8 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ * getCommandlineStr: `(filenames: string[]) => string` * 指定されたファイルを実行するためのコマンドライン引数文字列を返します。表示用です。 +## languages.ts + ### LangConstant 言語ごとに固定の定数です。 diff --git a/packages/runtime/package.json b/packages/runtime/package.json new file mode 100644 index 00000000..edaa20be --- /dev/null +++ b/packages/runtime/package.json @@ -0,0 +1,38 @@ +{ + "name": "@my-code/runtime", + "private": true, + "type": "module", + "exports": { + "./*": "./src/*", + "./samples/*": "./samples/*" + }, + "dependencies": { + "@ruby/wasm-wasi": "^2.7.2", + "@typescript/vfs": "^1.6.2", + "@xterm/addon-fit": "0.11.0-beta.115", + "@xterm/xterm": "5.6.0-beta.115", + "ace-builds": "^1.43.2", + "async-mutex": "^0.5.0", + "chai": "^6.2.0", + "chalk": "^5.5.0", + "comlink": "^4.4.2", + "mocha": "^11.7.4", + "object-inspect": "^1.13.4", + "prismjs": "^1.30.0", + "pyodide": "^0.29.0", + "react": "^19", + "react-ace": "^14.0.1", + "react-dom": "^19", + "react-syntax-highlighter": "^16.1.0", + "typescript": "^5" + }, + "devDependencies": { + "@types/chai": "^5.2.3", + "@types/mocha": "^10.0.10", + "@types/object-inspect": "^1.13.0", + "@types/prismjs": "^1.26.5", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/react-syntax-highlighter": "^15.5.13" + } +} diff --git a/app/terminal/samples/main.cpp b/packages/runtime/samples/main.cpp similarity index 100% rename from app/terminal/samples/main.cpp rename to packages/runtime/samples/main.cpp diff --git a/app/terminal/samples/main.js b/packages/runtime/samples/main.js similarity index 100% rename from app/terminal/samples/main.js rename to packages/runtime/samples/main.js diff --git a/app/terminal/samples/main.py b/packages/runtime/samples/main.py similarity index 100% rename from app/terminal/samples/main.py rename to packages/runtime/samples/main.py diff --git a/app/terminal/samples/main.rb b/packages/runtime/samples/main.rb similarity index 100% rename from app/terminal/samples/main.rb rename to packages/runtime/samples/main.rb diff --git a/app/terminal/samples/main2.rs b/packages/runtime/samples/main2.rs similarity index 100% rename from app/terminal/samples/main2.rs rename to packages/runtime/samples/main2.rs diff --git a/app/terminal/samples/main2.ts b/packages/runtime/samples/main2.ts similarity index 100% rename from app/terminal/samples/main2.ts rename to packages/runtime/samples/main2.ts diff --git a/app/terminal/samples/sub.cpp b/packages/runtime/samples/sub.cpp similarity index 100% rename from app/terminal/samples/sub.cpp rename to packages/runtime/samples/sub.cpp diff --git a/app/terminal/samples/sub.h b/packages/runtime/samples/sub.h similarity index 100% rename from app/terminal/samples/sub.h rename to packages/runtime/samples/sub.h diff --git a/app/terminal/samples/sub.rs b/packages/runtime/samples/sub.rs similarity index 100% rename from app/terminal/samples/sub.rs rename to packages/runtime/samples/sub.rs diff --git a/packages/runtime/src/context.tsx b/packages/runtime/src/context.tsx new file mode 100644 index 00000000..c82f1f41 --- /dev/null +++ b/packages/runtime/src/context.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { ReactNode, useEffect } from "react"; +import { RuntimeContext } from "./interface"; +import { RuntimeLang } from "./languages"; +import { TypeScriptProvider, useTypeScript } from "./typescript/runtime"; +import { useWandbox, WandboxProvider } from "./wandbox/runtime"; +import { JSEvalContext, useJSEval } from "./worker/jsEval"; +import { PyodideContext, usePyodide } from "./worker/pyodide"; +import { RubyContext, useRuby } from "./worker/ruby"; +import { WorkerProvider } from "./worker/runtime"; + +export function useRuntime(language: RuntimeLang): RuntimeContext { + // すべての言語のcontextをインスタンス化 + const pyodide = usePyodide(); + const ruby = useRuby(); + const jsEval = useJSEval(); + const typescript = useTypeScript(jsEval); + const wandboxCpp = useWandbox("cpp"); + const wandboxRust = useWandbox("rust"); + + let runtime: RuntimeContext; + switch (language) { + case "python": + runtime = pyodide; + break; + case "ruby": + runtime = ruby; + break; + case "javascript": + runtime = jsEval; + break; + case "typescript": + runtime = typescript; + break; + case "cpp": + runtime = wandboxCpp; + break; + case "rust": + runtime = wandboxRust; + break; + default: + language satisfies never; + throw new Error(`Runtime not implemented for language: ${language}`); + } + const { init } = runtime; + useEffect(() => { + init?.(); + }, [init]); + return runtime; +} +export function RuntimeProvider({ children }: { children: ReactNode }) { + return ( + + + + + {children} + + + + + ); +} diff --git a/app/terminal/editor.tsx b/packages/runtime/src/editor.tsx similarity index 99% rename from app/terminal/editor.tsx rename to packages/runtime/src/editor.tsx index 4def9eda..52e0bd34 100644 --- a/app/terminal/editor.tsx +++ b/packages/runtime/src/editor.tsx @@ -4,7 +4,7 @@ import { lazy, Suspense, useEffect, useState } from "react"; import clsx from "clsx"; import { useChangeTheme } from "@/themeToggle"; import { useEmbedContext } from "./embedContext"; -import { langConstants } from "./runtime"; +import { langConstants } from "./languages"; import { MarkdownLang } from "@/[lang]/[pageId]/styledSyntaxHighlighter"; // https://github.com/securingsincity/react-ace/issues/27 により普通のimportができない diff --git a/app/terminal/embedContext.tsx b/packages/runtime/src/embedContext.tsx similarity index 100% rename from app/terminal/embedContext.tsx rename to packages/runtime/src/embedContext.tsx diff --git a/app/terminal/exec.tsx b/packages/runtime/src/exec.tsx similarity index 98% rename from app/terminal/exec.tsx rename to packages/runtime/src/exec.tsx index eed8414f..62a41b1d 100644 --- a/app/terminal/exec.tsx +++ b/packages/runtime/src/exec.tsx @@ -10,8 +10,9 @@ import { import { writeOutput } from "./repl"; import { useEffect, useState } from "react"; import { useEmbedContext } from "./embedContext"; -import { RuntimeLang, useRuntime } from "./runtime"; +import { RuntimeLang } from "./languages"; import clsx from "clsx"; +import { useRuntime } from "./context"; interface ExecProps { /* diff --git a/app/terminal/highlight.ts b/packages/runtime/src/highlight.ts similarity index 98% rename from app/terminal/highlight.ts rename to packages/runtime/src/highlight.ts index 39b97310..d3612ce0 100644 --- a/app/terminal/highlight.ts +++ b/packages/runtime/src/highlight.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; chalk.level = 3; -import { RuntimeLang } from "./runtime"; +import { RuntimeLang } from "./languages"; export async function importPrism() { if (typeof window !== "undefined") { diff --git a/app/terminal/icons.tsx b/packages/runtime/src/icons.tsx similarity index 99% rename from app/terminal/icons.tsx rename to packages/runtime/src/icons.tsx index 22509580..50ccccc9 100644 --- a/app/terminal/icons.tsx +++ b/packages/runtime/src/icons.tsx @@ -1,4 +1,4 @@ -import { RuntimeLang } from "./runtime"; +import { RuntimeLang } from "./languages"; interface Props { lang: RuntimeLang; diff --git a/packages/runtime/src/interface.ts b/packages/runtime/src/interface.ts new file mode 100644 index 00000000..be3aacaa --- /dev/null +++ b/packages/runtime/src/interface.ts @@ -0,0 +1,54 @@ +import { MutexInterface } from "async-mutex"; +import { ReplCommand, ReplOutput, SyntaxStatus } from "./repl"; + +/** + * Common runtime context interface for different languages + * + * see README.md for details + * + */ +export interface RuntimeContext { + init?: () => void; + ready: boolean; + mutex?: MutexInterface; + interrupt?: () => void; + // repl + runCommand?: ( + command: string, + onOutput: (output: ReplOutput) => void + ) => Promise; + checkSyntax?: (code: string) => Promise; + splitReplExamples?: (content: string) => ReplCommand[]; + // file + runFiles: ( + filenames: string[], + files: Readonly>, + onOutput: (output: ReplOutput) => void + ) => Promise; + getCommandlineStr?: (filenames: string[]) => string; + runtimeInfo?: RuntimeInfo; +} +export interface RuntimeInfo { + prettyLangName: string; + version?: string; +} +export interface LangConstants { + tabSize: number; + prompt?: string; + promptMore?: string; + returnPrefix?: string; +} + +export const emptyMutex: MutexInterface = { + async runExclusive(fn: () => Promise | T) { + const result = fn(); + return result instanceof Promise ? result : Promise.resolve(result); + }, + acquire: async () => { + return () => {}; // Release function (no-op) + }, + waitForUnlock: async () => {}, + isLocked: () => false, + cancel: () => {}, + release: () => {}, +}; diff --git a/packages/runtime/src/languages.ts b/packages/runtime/src/languages.ts new file mode 100644 index 00000000..5a3d84ba --- /dev/null +++ b/packages/runtime/src/languages.ts @@ -0,0 +1,108 @@ +"use client"; + +import { AceLang } from "./editor"; +import { MarkdownLang } from "@/[lang]/[pageId]/styledSyntaxHighlighter"; +import { LangConstants } from "./interface"; + +export type RuntimeLang = + | "python" + | "ruby" + | "cpp" + | "rust" + | "javascript" + | "typescript"; + +export function getRuntimeLang( + lang: MarkdownLang | undefined +): RuntimeLang | undefined { + // markdownで指定される可能性のある言語名からRuntimeLangを取得 + switch (lang) { + case "python": + case "py": + return "python"; + case "ruby": + case "rb": + return "ruby"; + case "cpp": + case "c++": + return "cpp"; + case "rust": + case "rs": + return "rust"; + case "javascript": + case "js": + return "javascript"; + case "typescript": + case "ts": + return "typescript"; + case "bash": + case "sh": + case "powershell": + case "json": + case "toml": + case "csv": + case "text": + case "txt": + case "html": + case "makefile": + case "cmake": + case undefined: + // unsupported languages + return undefined; + default: + lang satisfies never; + console.error(`getRuntimeLang() does not handle language ${lang}`); + return undefined; + } +} + +export function langConstants(lang: RuntimeLang | AceLang): LangConstants { + switch (lang) { + case "python": + return { + tabSize: 4, + prompt: ">>> ", + promptMore: "... ", + }; + case "ruby": + return { + tabSize: 2, + // TODO: 実際のirbのプロンプトは静的でなく、(main)や番号などの動的な表示がある + prompt: "irb> ", + promptMore: "irb* ", + returnPrefix: "=> ", + }; + case "javascript": + case "typescript": + return { + tabSize: 2, + prompt: "> ", + promptMore: "... ", + }; + case "c_cpp": + case "cpp": + return { + // 2文字派と4文字派があるが、geminiが4文字で出力するので4でいいや + tabSize: 4, + }; + case "rust": + return { + tabSize: 4, + }; + case "json": + return { + // python-7章で使っている + tabSize: 4, + }; + case "csv": + case "text": + return { + // tabは使わないが、0は指定できないようなので適当にデフォルト値 + tabSize: 4, + }; + default: + lang satisfies never; + throw new Error(`LangConstants not defined for language: ${lang}`); + } +} + diff --git a/app/terminal/repl.tsx b/packages/runtime/src/repl.tsx similarity index 99% rename from app/terminal/repl.tsx rename to packages/runtime/src/repl.tsx index bca122bf..9b6cac1d 100644 --- a/app/terminal/repl.tsx +++ b/packages/runtime/src/repl.tsx @@ -14,9 +14,11 @@ import { } from "./terminal"; import type { Terminal } from "@xterm/xterm"; import { useEmbedContext } from "./embedContext"; -import { emptyMutex, langConstants, RuntimeLang, useRuntime } from "./runtime"; +import { langConstants, RuntimeLang } from "./languages"; import clsx from "clsx"; import { InlineCode } from "@/[lang]/[pageId]/markdown"; +import { emptyMutex } from "./interface"; +import { useRuntime } from "./context"; export type ReplOutputType = | "stdout" diff --git a/app/terminal/terminal.tsx b/packages/runtime/src/terminal.tsx similarity index 100% rename from app/terminal/terminal.tsx rename to packages/runtime/src/terminal.tsx diff --git a/app/terminal/tests.ts b/packages/runtime/src/tests.ts similarity index 99% rename from app/terminal/tests.ts rename to packages/runtime/src/tests.ts index 60a7701a..3ac8925e 100644 --- a/app/terminal/tests.ts +++ b/packages/runtime/src/tests.ts @@ -1,7 +1,8 @@ import { expect } from "chai"; import { RefObject } from "react"; -import { emptyMutex, RuntimeContext, RuntimeLang } from "./runtime"; import { ReplOutput } from "./repl"; +import { RuntimeLang } from "./languages"; +import { emptyMutex, RuntimeContext } from "./interface"; export function defineTests( lang: RuntimeLang, diff --git a/app/terminal/typescript/runtime.tsx b/packages/runtime/src/typescript/runtime.tsx similarity index 98% rename from app/terminal/typescript/runtime.tsx rename to packages/runtime/src/typescript/runtime.tsx index 8637b002..1aabcc73 100644 --- a/app/terminal/typescript/runtime.tsx +++ b/packages/runtime/src/typescript/runtime.tsx @@ -13,7 +13,7 @@ import { } from "react"; import { useEmbedContext } from "../embedContext"; import { ReplOutput } from "../repl"; -import { RuntimeContext, RuntimeInfo } from "../runtime"; +import { RuntimeContext, RuntimeInfo } from "../interface"; export const compilerOptions: CompilerOptions = { lib: ["ESNext", "WebWorker"], diff --git a/app/terminal/wandbox/api.ts b/packages/runtime/src/wandbox/api.ts similarity index 99% rename from app/terminal/wandbox/api.ts rename to packages/runtime/src/wandbox/api.ts index 6691b663..a3d6b7e4 100644 --- a/app/terminal/wandbox/api.ts +++ b/packages/runtime/src/wandbox/api.ts @@ -1,6 +1,6 @@ import { type Fetcher } from "swr"; import { type ReplOutput } from "../repl"; -import { RuntimeInfo } from "../runtime"; +import { RuntimeInfo } from "../interface"; const WANDBOX = "https://wandbox.org"; // https://github.com/melpon/wandbox/blob/ajax/kennel2/API.rst <- 古いけど、説明と例がある diff --git a/app/terminal/wandbox/cpp.ts b/packages/runtime/src/wandbox/cpp.ts similarity index 100% rename from app/terminal/wandbox/cpp.ts rename to packages/runtime/src/wandbox/cpp.ts diff --git a/app/terminal/wandbox/cpp/_stacktrace.cpp b/packages/runtime/src/wandbox/cpp/_stacktrace.cpp similarity index 100% rename from app/terminal/wandbox/cpp/_stacktrace.cpp rename to packages/runtime/src/wandbox/cpp/_stacktrace.cpp diff --git a/app/terminal/wandbox/runtime.tsx b/packages/runtime/src/wandbox/runtime.tsx similarity index 97% rename from app/terminal/wandbox/runtime.tsx rename to packages/runtime/src/wandbox/runtime.tsx index c7d069bf..a919caa8 100644 --- a/app/terminal/wandbox/runtime.tsx +++ b/packages/runtime/src/wandbox/runtime.tsx @@ -10,9 +10,10 @@ import { import useSWR from "swr"; import { compilerInfoFetcher, SelectedCompiler } from "./api"; import { cppRunFiles, selectCppCompiler } from "./cpp"; -import { RuntimeContext, RuntimeInfo, RuntimeLang } from "../runtime"; +import { RuntimeLang } from "../languages"; import { ReplOutput } from "../repl"; import { rustRunFiles, selectRustCompiler } from "./rust"; +import { RuntimeContext, RuntimeInfo } from "../interface"; type WandboxLang = "cpp" | "rust"; diff --git a/app/terminal/wandbox/rust.ts b/packages/runtime/src/wandbox/rust.ts similarity index 100% rename from app/terminal/wandbox/rust.ts rename to packages/runtime/src/wandbox/rust.ts diff --git a/app/terminal/wandbox/rust/prog.rs b/packages/runtime/src/wandbox/rust/prog.rs similarity index 100% rename from app/terminal/wandbox/rust/prog.rs rename to packages/runtime/src/wandbox/rust/prog.rs diff --git a/app/terminal/worker/jsEval.ts b/packages/runtime/src/worker/jsEval.ts similarity index 96% rename from app/terminal/worker/jsEval.ts rename to packages/runtime/src/worker/jsEval.ts index cf022d69..dde525b6 100644 --- a/app/terminal/worker/jsEval.ts +++ b/packages/runtime/src/worker/jsEval.ts @@ -1,7 +1,7 @@ "use client"; import { createContext, useContext } from "react"; -import { RuntimeContext, RuntimeInfo } from "../runtime"; +import { RuntimeContext, RuntimeInfo } from "../interface"; import { ReplCommand, ReplOutput } from "../repl"; export const JSEvalContext = createContext(null!); diff --git a/app/terminal/worker/jsEval.worker.ts b/packages/runtime/src/worker/jsEval.worker.ts similarity index 100% rename from app/terminal/worker/jsEval.worker.ts rename to packages/runtime/src/worker/jsEval.worker.ts diff --git a/app/terminal/worker/pyodide.ts b/packages/runtime/src/worker/pyodide.ts similarity index 96% rename from app/terminal/worker/pyodide.ts rename to packages/runtime/src/worker/pyodide.ts index e75a223c..6c2d3c65 100644 --- a/app/terminal/worker/pyodide.ts +++ b/packages/runtime/src/worker/pyodide.ts @@ -1,7 +1,7 @@ "use client"; import { createContext, useContext } from "react"; -import { RuntimeContext, RuntimeInfo } from "../runtime"; +import { RuntimeContext, RuntimeInfo } from "../interface"; import { ReplCommand, ReplOutput } from "../repl"; import pyodideLock from "pyodide/pyodide-lock.json"; diff --git a/app/terminal/worker/pyodide.worker.ts b/packages/runtime/src/worker/pyodide.worker.ts similarity index 100% rename from app/terminal/worker/pyodide.worker.ts rename to packages/runtime/src/worker/pyodide.worker.ts diff --git a/app/terminal/worker/pyodide/check_syntax.py b/packages/runtime/src/worker/pyodide/check_syntax.py similarity index 100% rename from app/terminal/worker/pyodide/check_syntax.py rename to packages/runtime/src/worker/pyodide/check_syntax.py diff --git a/app/terminal/worker/pyodide/execfile.py b/packages/runtime/src/worker/pyodide/execfile.py similarity index 100% rename from app/terminal/worker/pyodide/execfile.py rename to packages/runtime/src/worker/pyodide/execfile.py diff --git a/app/terminal/worker/ruby.ts b/packages/runtime/src/worker/ruby.ts similarity index 96% rename from app/terminal/worker/ruby.ts rename to packages/runtime/src/worker/ruby.ts index 2fa79de8..ef711934 100644 --- a/app/terminal/worker/ruby.ts +++ b/packages/runtime/src/worker/ruby.ts @@ -1,7 +1,7 @@ "use client"; import { createContext, useContext } from "react"; -import { RuntimeContext, RuntimeInfo } from "../runtime"; +import { RuntimeContext, RuntimeInfo } from "../interface"; import { ReplCommand, ReplOutput } from "../repl"; export const RubyContext = createContext(null!); diff --git a/app/terminal/worker/ruby.worker.ts b/packages/runtime/src/worker/ruby.worker.ts similarity index 100% rename from app/terminal/worker/ruby.worker.ts rename to packages/runtime/src/worker/ruby.worker.ts diff --git a/app/terminal/worker/ruby/init.rb b/packages/runtime/src/worker/ruby/init.rb similarity index 100% rename from app/terminal/worker/ruby/init.rb rename to packages/runtime/src/worker/ruby/init.rb diff --git a/app/terminal/worker/runtime.tsx b/packages/runtime/src/worker/runtime.tsx similarity index 98% rename from app/terminal/worker/runtime.tsx rename to packages/runtime/src/worker/runtime.tsx index 6ada8525..c0ef70f4 100644 --- a/app/terminal/worker/runtime.tsx +++ b/packages/runtime/src/worker/runtime.tsx @@ -10,10 +10,11 @@ import { useState, } from "react"; import { wrap, Remote, proxy } from "comlink"; -import { RuntimeContext, RuntimeLang } from "../runtime"; +import { RuntimeLang } from "../languages"; import { ReplOutput, SyntaxStatus } from "../repl"; import { Mutex, MutexInterface } from "async-mutex"; import { useEmbedContext } from "../embedContext"; +import { RuntimeContext } from "../interface"; type WorkerLang = "python" | "ruby" | "javascript"; export type WorkerCapabilities = { diff --git a/scripts/copyAllDTSFiles.ts b/scripts/copyAllDTSFiles.ts index effa3c27..881c170a 100644 --- a/scripts/copyAllDTSFiles.ts +++ b/scripts/copyAllDTSFiles.ts @@ -1,7 +1,7 @@ // node_modules/typescript/lib からd.tsファイルをすべてpublic/typescript/version/にコピーする。 import { knownLibFilesForCompilerOptions } from "@typescript/vfs"; -import { compilerOptions } from "../app/terminal/typescript/runtime"; +import { compilerOptions } from "@my-code/runtime/typescript/runtime"; import ts from "typescript"; import fs from "node:fs/promises"; import { existsSync } from "node:fs"; diff --git a/tsconfig.json b/tsconfig.json index fc693c61..09d537f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,9 @@ } ], "paths": { - "@/*": ["./app/*"] + "@/*": ["./app/*"], + "@my-code/runtime/samples/*": ["./packages/runtime/samples/*"], + "@my-code/runtime/*": ["./packages/runtime/src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],