From 63c6ad2d9abb470cf0a426ab83e2d8ed7e99ade8 Mon Sep 17 00:00:00 2001
From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com>
Date: Mon, 3 Aug 2026 00:40:55 +0900
Subject: [PATCH 1/3] add Dart
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- **API**: DartPad の `compileNewDDC` API エンドポイント (`https://stable.api.dartpad.dev/api/v3/compileNewDDC`) を使用。
- **実行環境**: iframe 内で RequireJS により `ddc_module_loader.js` および `dart_sdk_new.js` をロードし、`dartDevEmbedder.runMain` でコンパイル済み JS を安全に実行。
- **UI & 統合**: Ace Editor モード (`mode-dart`)、Dart SVG アイコン、テスト用サンプルコード (`main.dart`) を追加・統合。
---
app/terminal/editor.tsx | 1 +
app/terminal/icons.tsx | 10 +
app/terminal/page.tsx | 8 +
app/terminal/samples/main.dart | 3 +
packages/runtime/src/context.tsx | 8 +-
packages/runtime/src/dart/runtime.tsx | 299 ++++++++++++++++++++++++
packages/runtime/src/languages.ts | 21 +-
packages/runtime/tests/fileExecution.ts | 7 +
packages/runtime/tests/repl.ts | 5 +
packages/runtime/tests/utils.ts | 1 +
10 files changed, 358 insertions(+), 5 deletions(-)
create mode 100644 app/terminal/samples/main.dart
create mode 100644 packages/runtime/src/dart/runtime.tsx
diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx
index 14d2547b..70e9b94e 100644
--- a/app/terminal/editor.tsx
+++ b/app/terminal/editor.tsx
@@ -26,6 +26,7 @@ const AceEditor = lazy(async () => {
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");
+ await import("ace-builds/src-min-noconflict/mode-dart");
return ace;
} else {
throw new Error("should not try SSR");
diff --git a/app/terminal/icons.tsx b/app/terminal/icons.tsx
index 872ae930..9c356a0f 100644
--- a/app/terminal/icons.tsx
+++ b/app/terminal/icons.tsx
@@ -113,6 +113,16 @@ export function LanguageIcon(props: Props) {
);
+ case "dart":
+ return (
+
+ );
default:
props.lang satisfies never;
console.warn("unknown lang for LanguageIcon:", props.lang);
diff --git a/app/terminal/page.tsx b/app/terminal/page.tsx
index ba9a32ec..b40bef1c 100644
--- a/app/terminal/page.tsx
+++ b/app/terminal/page.tsx
@@ -26,6 +26,7 @@ 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_dart from "./samples/main.dart?raw";
import { DaisyInfoIcon } from "@/daisyAlertIcon";
export default function RuntimeTestPage() {
@@ -127,6 +128,13 @@ const sampleConfig: Record = {
},
exec: ["main2.rs"],
},
+ dart: {
+ repl: false,
+ editor: {
+ "main.dart": main_dart,
+ },
+ exec: ["main.dart"],
+ },
};
function RuntimeSample({
lang,
diff --git a/app/terminal/samples/main.dart b/app/terminal/samples/main.dart
new file mode 100644
index 00000000..a506f97c
--- /dev/null
+++ b/app/terminal/samples/main.dart
@@ -0,0 +1,3 @@
+void main() {
+ print("Hello, Dart!");
+}
diff --git a/packages/runtime/src/context.tsx b/packages/runtime/src/context.tsx
index 9874812e..4a7e5ac4 100644
--- a/packages/runtime/src/context.tsx
+++ b/packages/runtime/src/context.tsx
@@ -3,6 +3,7 @@
import { ReactNode, useEffect } from "react";
import { RuntimeContext } from "./interface";
import { RuntimeLang } from "./languages";
+import { DartProvider, useDart } from "./dart/runtime";
import { TypeScriptProvider, useTypeScript } from "./typescript/runtime";
import { useWandbox, WandboxProvider } from "./wandbox/runtime";
import { JSEvalContext, useJSEval } from "./worker/jsEval";
@@ -34,6 +35,7 @@ export function useRuntimeAll(): Record {
const typescript = useTypeScript(jsEval);
const wandboxCpp = useWandbox("cpp");
const wandboxRust = useWandbox("rust");
+ const dart = useDart();
// initはしない。呼び出し側でする必要がある
return {
@@ -43,6 +45,7 @@ export function useRuntimeAll(): Record {
typescript: typescript,
cpp: wandboxCpp,
rust: wandboxRust,
+ dart: dart,
};
}
export function RuntimeProvider({ children }: { children: ReactNode }) {
@@ -51,10 +54,13 @@ export function RuntimeProvider({ children }: { children: ReactNode }) {
- {children}
+
+ {children}
+
);
}
+
diff --git a/packages/runtime/src/dart/runtime.tsx b/packages/runtime/src/dart/runtime.tsx
new file mode 100644
index 00000000..dabf52fa
--- /dev/null
+++ b/packages/runtime/src/dart/runtime.tsx
@@ -0,0 +1,299 @@
+"use client";
+
+import {
+ createContext,
+ ReactNode,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+} from "react";
+import useSWR from "swr";
+import {
+ ReplOutput,
+ RuntimeContext,
+ RuntimeErrorHandler,
+ RuntimeInfo,
+ UpdatedFile,
+} from "../interface";
+
+const DART_PAD_API_BASE = "https://stable.api.dartpad.dev/api/v3";
+const DART_PAD_ARTIFACTS_BASE = "https://stable.api.dartpad.dev/artifacts";
+
+interface DartVersionResponse {
+ dartVersion?: string;
+ flutterVersion?: string;
+}
+
+const versionFetcher = async (url: string): Promise => {
+ const res = await fetch(url);
+ if (!res.ok) {
+ throw new Error(`Failed to fetch Dart version: ${res.statusText}`);
+ }
+ return res.json();
+};
+
+const DartContext = createContext<{
+ init: (onError?: RuntimeErrorHandler) => void;
+ ready: boolean;
+ dartVersion?: string;
+}>({
+ init: () => undefined,
+ ready: true,
+});
+
+export function DartProvider({ children }: { children: ReactNode }) {
+ const onErrorRef = useRef(undefined);
+ const init = useCallback((onError?: RuntimeErrorHandler) => {
+ onErrorRef.current = onError;
+ }, []);
+
+ const { data, error } = useSWR(
+ `${DART_PAD_API_BASE}/version`,
+ versionFetcher
+ );
+
+ useEffect(() => {
+ if (error) {
+ console.error("Failed to fetch Dart version info:", error);
+ onErrorRef.current?.(error);
+ }
+ }, [error]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useDart(): RuntimeContext {
+ const { init: dartInit, ready, dartVersion } = useContext(DartContext);
+ const onErrorRef = useRef(undefined);
+
+ const init = useCallback(
+ (onError?: RuntimeErrorHandler) => {
+ onErrorRef.current = onError;
+ dartInit(onError);
+ },
+ [dartInit]
+ );
+
+ const runFiles = useCallback(
+ async (
+ filenames: string[],
+ files: Readonly>,
+ onOutput: (output: ReplOutput | UpdatedFile) => void
+ ) => {
+ if (typeof window === "undefined") {
+ onOutput({
+ type: "error",
+ message: "Dart runtime requires browser environment.",
+ });
+ return;
+ }
+
+ const filename = filenames[0] ?? Object.keys(files)[0];
+ const source = files[filename] ?? Object.values(files)[0];
+
+ if (!source) {
+ onOutput({ type: "error", message: "No source code provided to run." });
+ return;
+ }
+
+ try {
+ const response = await fetch(`${DART_PAD_API_BASE}/compileNewDDC`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ source }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ onOutput({
+ type: "error",
+ message:
+ errorText ||
+ `Compilation failed with status ${response.status}`,
+ });
+ return;
+ }
+
+ const data = await response.json();
+ if (!data.result) {
+ onOutput({
+ type: "error",
+ message: "Compilation returned empty result.",
+ });
+ return;
+ }
+
+ const jsCode: string = data.result;
+
+ // Execute compiled JS inside a temporary iframe
+ await new Promise((resolve) => {
+ const iframe = document.createElement("iframe");
+ iframe.style.display = "none";
+ document.body.appendChild(iframe);
+
+ let resolved = false;
+ const cleanup = () => {
+ if (resolved) return;
+ resolved = true;
+ window.removeEventListener("message", handleMessage);
+ if (iframe.parentNode) {
+ iframe.parentNode.removeChild(iframe);
+ }
+ resolve();
+ };
+
+ const handleMessage = (event: MessageEvent) => {
+ if (event.source !== iframe.contentWindow) return;
+ const msgData = event.data;
+ if (!msgData || msgData.sender !== "dart_frame") return;
+
+ if (msgData.type === "stdout") {
+ onOutput({ type: "stdout", message: String(msgData.message) });
+ } else if (msgData.type === "stderr") {
+ onOutput({ type: "stderr", message: String(msgData.message) });
+ } else if (msgData.type === "done") {
+ cleanup();
+ } else if (msgData.type === "error") {
+ onOutput({ type: "error", message: String(msgData.message) });
+ cleanup();
+ }
+ };
+
+ window.addEventListener("message", handleMessage);
+
+ const iframeDoc = iframe.contentDocument;
+ if (!iframeDoc) {
+ onOutput({
+ type: "error",
+ message: "Failed to access iframe document.",
+ });
+ cleanup();
+ return;
+ }
+
+ const htmlContent = `
+
+
+
+
+
+
+
+`;
+
+ iframeDoc.open();
+ iframeDoc.write(htmlContent);
+ iframeDoc.close();
+
+ setTimeout(() => {
+ cleanup();
+ }, 15000);
+ });
+ } catch (error) {
+ onErrorRef.current?.(error);
+ onOutput({
+ type: "fatalError",
+ message: error instanceof Error ? error.message : String(error),
+ });
+ }
+ },
+ []
+ );
+
+ const runtimeInfo = useMemo(
+ () => ({
+ prettyLangName: "Dart",
+ version: dartVersion,
+ }),
+ [dartVersion]
+ );
+
+ return {
+ init,
+ ready,
+ runFiles,
+ getCommandlineStr,
+ runtimeInfo,
+ };
+}
+
+function getCommandlineStr(filenames: string[]) {
+ return `dart run ${filenames[0] ?? "main.dart"}`;
+}
diff --git a/packages/runtime/src/languages.ts b/packages/runtime/src/languages.ts
index 3c2ceb72..2b4ff665 100644
--- a/packages/runtime/src/languages.ts
+++ b/packages/runtime/src/languages.ts
@@ -22,7 +22,8 @@ export type MarkdownLang =
| "makefile"
| "cmake"
| "text"
- | "txt";
+ | "txt"
+ | "dart";
export type RuntimeLang =
| "python"
@@ -30,7 +31,8 @@ export type RuntimeLang =
| "cpp"
| "rust"
| "javascript"
- | "typescript";
+ | "typescript"
+ | "dart";
export type LangConstants = {
originalLang: MarkdownLang | undefined;
@@ -50,7 +52,8 @@ export type LangConstants = {
| "json"
| "ini"
| "makefile"
- | "cmake";
+ | "cmake"
+ | "dart";
} & (
| {
// terminal/editor.tsx でimportする mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値と対応する
@@ -63,7 +66,8 @@ export type LangConstants = {
| "typescript"
| "json"
| "csv"
- | "text";
+ | "text"
+ | "dart";
tabSize: number;
}
| {
@@ -158,6 +162,14 @@ export function langConstants(lang: MarkdownLang | undefined): LangConstants {
tabSize: 4,
runtime: "rust",
};
+ case "dart":
+ return {
+ originalLang: lang,
+ rsh: "dart",
+ ace: "dart",
+ tabSize: 2,
+ runtime: "dart",
+ };
case "bash":
case "sh":
return { originalLang: lang, rsh: "bash" };
@@ -198,3 +210,4 @@ export function langConstants(lang: MarkdownLang | undefined): LangConstants {
return { originalLang: lang };
}
}
+
diff --git a/packages/runtime/tests/fileExecution.ts b/packages/runtime/tests/fileExecution.ts
index ee1b9617..88542853 100644
--- a/packages/runtime/tests/fileExecution.ts
+++ b/packages/runtime/tests/fileExecution.ts
@@ -20,6 +20,7 @@ export const fileExecutionTests: Record<
rust: ["test.rs", `fn main() {\n println!("${msg}");\n}\n`],
javascript: ["test.js", `console.log("${msg}")`],
typescript: ["test.ts", `console.log("${msg}")`],
+ dart: ["main.dart", `void main() {\n print("${msg}");\n}\n`],
} satisfies Record
)[lang];
if (!filename || !code) return null;
@@ -53,6 +54,10 @@ export const fileExecutionTests: Record<
rust: ["test_error.rs", `fn main() {\n panic!("${errorMsg}");\n}\n`],
javascript: ["test_error.js", `throw new Error("${errorMsg}");\n`],
typescript: ["test_error.ts", `throw new Error("${errorMsg}");\n`],
+ dart: [
+ "test_error.dart",
+ `void main() {\n throw Exception("${errorMsg}");\n}\n`,
+ ],
} satisfies Record
)[lang];
if (!filename || !code) return null;
@@ -114,6 +119,7 @@ export const fileExecutionTests: Record<
],
javascript: [null, null],
typescript: [null, null],
+ dart: [null, null],
} satisfies Record<
RuntimeLang,
[Record, string[]] | [null, null]
@@ -148,6 +154,7 @@ export const fileExecutionTests: Record<
rust: [null, null],
javascript: [null, null],
typescript: [null, null],
+ dart: [null, null],
} satisfies Record
)[lang];
if (!filename || !code) return null;
diff --git a/packages/runtime/tests/repl.ts b/packages/runtime/tests/repl.ts
index e47d5da3..999ff2b4 100644
--- a/packages/runtime/tests/repl.ts
+++ b/packages/runtime/tests/repl.ts
@@ -19,6 +19,7 @@ export const replTests: Record TestBody | null> =
rust: null,
javascript: `console.log("${msg}")`,
typescript: null,
+ dart: null,
} satisfies Record
)[lang];
if (!printCode) return null;
@@ -49,6 +50,7 @@ export const replTests: Record TestBody | null> =
`console.log(${varName})`,
],
typescript: [null, null],
+ dart: [null, null],
} satisfies Record
)[lang];
if (!setIntVarCode || !printIntVarCode) return null;
@@ -87,6 +89,7 @@ export const replTests: Record TestBody | null> =
rust: null,
javascript: `throw new Error("${errorMsg}")`,
typescript: null,
+ dart: null,
} satisfies Record
)[lang];
if (!errorCode) return null;
@@ -118,6 +121,7 @@ export const replTests: Record TestBody | null> =
`console.log(testVar)`,
],
typescript: [null, null, null],
+ dart: [null, null, null],
} satisfies Record
)[lang];
if (!setIntVarCode || !infLoopCode || !printIntVarCode) return null;
@@ -168,6 +172,7 @@ export const replTests: Record TestBody | null> =
rust: null,
javascript: null,
typescript: null,
+ dart: null,
} satisfies Record
)[lang];
if (!writeCode) return null;
diff --git a/packages/runtime/tests/utils.ts b/packages/runtime/tests/utils.ts
index f9878cb0..15c98d29 100644
--- a/packages/runtime/tests/utils.ts
+++ b/packages/runtime/tests/utils.ts
@@ -9,6 +9,7 @@ export const RUNTIME_TIMEOUTS: Record = {
typescript: 2000,
cpp: 10000,
rust: 20000,
+ dart: 15000,
};
export async function waitForRuntimeReady(
From bab04f9497b0bbac8c9629558c959e7589516920 Mon Sep 17 00:00:00 2001
From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com>
Date: Mon, 3 Aug 2026 02:59:41 +0900
Subject: [PATCH 2/3] =?UTF-8?q?DartPad=20=E3=81=AE=E9=9D=99=E7=9A=84?=
=?UTF-8?q?=E8=A7=A3=E6=9E=90=20API=E3=81=AE=E5=91=BC=E3=81=B3=E5=87=BA?=
=?UTF-8?q?=E3=81=97=E5=87=A6=E7=90=86=E3=82=92=E8=BF=BD=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`performAnalysis` の追加
- 解析結果に含まれるエラー・警告・情報(`issues`)を、行・列番号および修正提案(`correction`)を含めたフォーマットで出力コールバック (`onOutput`) に渡します。
---
packages/runtime/src/dart/runtime.tsx | 71 ++++++++++++++++++++++++---
1 file changed, 64 insertions(+), 7 deletions(-)
diff --git a/packages/runtime/src/dart/runtime.tsx b/packages/runtime/src/dart/runtime.tsx
index dabf52fa..13698da8 100644
--- a/packages/runtime/src/dart/runtime.tsx
+++ b/packages/runtime/src/dart/runtime.tsx
@@ -26,6 +26,25 @@ interface DartVersionResponse {
flutterVersion?: string;
}
+interface AnalysisIssue {
+ kind: "error" | "warning" | "info";
+ message: string;
+ location: {
+ charStart: number;
+ charLength: number;
+ line: number;
+ column: number;
+ };
+ code?: string;
+ correction?: string;
+ url?: string;
+}
+
+interface AnalysisResponse {
+ issues: AnalysisIssue[];
+ imports?: unknown[];
+}
+
const versionFetcher = async (url: string): Promise => {
const res = await fetch(url);
if (!res.ok) {
@@ -74,6 +93,41 @@ export function DartProvider({ children }: { children: ReactNode }) {
);
}
+async function performAnalysis(
+ source: string,
+ onOutput: (output: ReplOutput | UpdatedFile) => void
+): Promise {
+ try {
+ const res = await fetch(`${DART_PAD_API_BASE}/analyze`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ source }),
+ });
+
+ if (!res.ok) return;
+
+ const data: AnalysisResponse = await res.json();
+ if (Array.isArray(data.issues)) {
+ for (const issue of data.issues) {
+ const line = issue.location?.line ?? 1;
+ const column = issue.location?.column ?? 1;
+ const kindStr = (issue.kind || "info").toUpperCase();
+ const correctionStr = issue.correction ? ` (${issue.correction})` : "";
+ const formattedMsg = `[ANALYZER ${kindStr}] line ${line}:${column} - ${issue.message}${correctionStr}`;
+
+ onOutput({
+ type: issue.kind === "error" ? "error" : "stderr",
+ message: formattedMsg,
+ });
+ }
+ }
+ } catch (err) {
+ console.warn("Failed to perform Dart static analysis:", err);
+ }
+}
+
export function useDart(): RuntimeContext {
const { init: dartInit, ready, dartVersion } = useContext(DartContext);
const onErrorRef = useRef(undefined);
@@ -109,13 +163,16 @@ export function useDart(): RuntimeContext {
}
try {
- const response = await fetch(`${DART_PAD_API_BASE}/compileNewDDC`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ source }),
- });
+ const [_, response] = await Promise.all([
+ performAnalysis(source, onOutput),
+ fetch(`${DART_PAD_API_BASE}/compileNewDDC`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ source }),
+ }),
+ ]);
if (!response.ok) {
const errorText = await response.text();
From 0147fe2f65609b86e7650ed6659a593a2c87aba3 Mon Sep 17 00:00:00 2001
From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com>
Date: Mon, 3 Aug 2026 04:32:17 +0900
Subject: [PATCH 3/3] =?UTF-8?q?interrupt=E5=AE=9F=E8=A3=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/runtime/src/dart/runtime.tsx | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/packages/runtime/src/dart/runtime.tsx b/packages/runtime/src/dart/runtime.tsx
index 13698da8..b94bcdac 100644
--- a/packages/runtime/src/dart/runtime.tsx
+++ b/packages/runtime/src/dart/runtime.tsx
@@ -131,6 +131,7 @@ async function performAnalysis(
export function useDart(): RuntimeContext {
const { init: dartInit, ready, dartVersion } = useContext(DartContext);
const onErrorRef = useRef(undefined);
+ const activeIframeRef = useRef(null);
const init = useCallback(
(onError?: RuntimeErrorHandler) => {
@@ -140,6 +141,15 @@ export function useDart(): RuntimeContext {
[dartInit]
);
+ const interrupt = useCallback(() => {
+ if (activeIframeRef.current) {
+ if (activeIframeRef.current.parentNode) {
+ activeIframeRef.current.parentNode.removeChild(activeIframeRef.current);
+ }
+ activeIframeRef.current = null;
+ }
+ }, []);
+
const runFiles = useCallback(
async (
filenames: string[],
@@ -201,12 +211,16 @@ export function useDart(): RuntimeContext {
const iframe = document.createElement("iframe");
iframe.style.display = "none";
document.body.appendChild(iframe);
+ activeIframeRef.current = iframe;
let resolved = false;
const cleanup = () => {
if (resolved) return;
resolved = true;
window.removeEventListener("message", handleMessage);
+ if (activeIframeRef.current === iframe) {
+ activeIframeRef.current = null;
+ }
if (iframe.parentNode) {
iframe.parentNode.removeChild(iframe);
}
@@ -346,6 +360,7 @@ export function useDart(): RuntimeContext {
init,
ready,
runFiles,
+ interrupt,
getCommandlineStr,
runtimeInfo,
};