Skip to content

Commit 63c6ad2

Browse files
committed
add Dart
- **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`) を追加・統合。
1 parent 66a0d41 commit 63c6ad2

10 files changed

Lines changed: 358 additions & 5 deletions

File tree

‎app/terminal/editor.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const AceEditor = lazy(async () => {
2626
await import("ace-builds/src-min-noconflict/mode-json");
2727
await import("ace-builds/src-min-noconflict/mode-csv");
2828
await import("ace-builds/src-min-noconflict/mode-text");
29+
await import("ace-builds/src-min-noconflict/mode-dart");
2930
return ace;
3031
} else {
3132
throw new Error("should not try SSR");

‎app/terminal/icons.tsx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ export function LanguageIcon(props: Props) {
113113
<path d="M31.584 15.615l-1.329-0.823c-0.009-0.131-0.020-0.261-0.036-0.385l1.141-1.063c0.244-0.235 0.161-0.647-0.152-0.765l-1.459-0.543c-0.036-0.129-0.072-0.255-0.113-0.38l0.905-1.26c0.204-0.276 0.043-0.661-0.296-0.719l-1.537-0.251c-0.057-0.115-0.12-0.229-0.181-0.344l0.645-1.416c0.068-0.145 0.052-0.312-0.036-0.443-0.089-0.131-0.24-0.208-0.396-0.203l-1.563 0.057c-0.079-0.105-0.161-0.204-0.245-0.303l0.36-1.516c0.077-0.333-0.219-0.629-0.552-0.552l-1.516 0.36c-0.099-0.084-0.197-0.167-0.303-0.245l0.057-1.563c0.016-0.339-0.339-0.577-0.645-0.432l-1.416 0.645c-0.115-0.061-0.229-0.124-0.349-0.181l-0.251-1.537c-0.057-0.333-0.443-0.495-0.719-0.296l-1.26 0.905c-0.125-0.041-0.251-0.077-0.375-0.113l-0.548-1.453c-0.119-0.319-0.531-0.401-0.76-0.157l-1.063 1.147c-0.129-0.016-0.26-0.032-0.391-0.041l-0.817-1.324c-0.177-0.291-0.599-0.291-0.776 0l-0.823 1.324c-0.131 0.009-0.26 0.025-0.385 0.041l-1.063-1.147c-0.235-0.244-0.645-0.167-0.765 0.157l-0.543 1.453c-0.129 0.036-0.249 0.077-0.38 0.113l-1.26-0.905c-0.276-0.199-0.661-0.037-0.719 0.296l-0.251 1.537c-0.115 0.057-0.228 0.12-0.343 0.181l-1.417-0.645c-0.312-0.14-0.661 0.093-0.645 0.432l0.052 1.563c-0.099 0.079-0.199 0.161-0.297 0.245l-1.52-0.36c-0.329-0.077-0.625 0.219-0.548 0.552l0.355 1.516c-0.084 0.099-0.161 0.197-0.245 0.303l-1.557-0.057c-0.343-0.011-0.572 0.339-0.432 0.645l0.647 1.416c-0.063 0.115-0.125 0.229-0.188 0.344l-1.536 0.251c-0.333 0.057-0.491 0.443-0.297 0.719l0.912 1.26c-0.043 0.131-0.079 0.251-0.115 0.38l-1.459 0.543c-0.317 0.119-0.396 0.531-0.151 0.765l1.14 1.063c-0.016 0.124-0.027 0.255-0.036 0.385l-1.328 0.823c-0.292 0.172-0.292 0.599 0 0.771l1.328 0.823c0.009 0.131 0.020 0.261 0.036 0.385l-1.14 1.068c-0.251 0.229-0.167 0.641 0.151 0.76l1.459 0.548c0.036 0.124 0.072 0.249 0.115 0.375l-0.912 1.26c-0.197 0.276-0.036 0.667 0.301 0.719l1.537 0.251c0.057 0.115 0.12 0.229 0.183 0.344l-0.647 1.416c-0.14 0.312 0.095 0.661 0.432 0.645l1.557-0.052c0.084 0.1 0.161 0.199 0.251 0.297l-0.36 1.521c-0.077 0.328 0.219 0.624 0.548 0.547l1.52-0.355c0.099 0.084 0.199 0.161 0.303 0.245l-0.057 1.557c-0.016 0.343 0.339 0.577 0.645 0.432l1.417-0.645c0.115 0.061 0.228 0.124 0.343 0.187l0.256 1.537c0.052 0.333 0.437 0.489 0.713 0.296l1.265-0.911c0.125 0.041 0.251 0.077 0.375 0.113l0.548 1.459c0.115 0.323 0.525 0.401 0.76 0.152l1.063-1.141c0.129 0.016 0.255 0.032 0.385 0.041l0.823 1.328c0.177 0.287 0.599 0.287 0.776 0l0.823-1.328c0.131-0.009 0.256-0.025 0.385-0.041l1.063 1.141c0.235 0.244 0.641 0.167 0.76-0.152l0.548-1.459c0.124-0.036 0.249-0.072 0.375-0.113l1.26 0.911c0.276 0.203 0.667 0.037 0.719-0.296l0.251-1.537c0.12-0.063 0.235-0.125 0.349-0.187l1.416 0.645c0.307 0.14 0.656-0.093 0.645-0.432l-0.057-1.557c0.105-0.084 0.204-0.161 0.297-0.245l1.521 0.355c0.328 0.077 0.624-0.219 0.552-0.547l-0.36-1.521c0.084-0.099 0.167-0.197 0.245-0.297l1.563 0.052c0.339 0.016 0.572-0.333 0.432-0.645l-0.645-1.416c0.061-0.115 0.124-0.229 0.181-0.344l1.537-0.251c0.333-0.057 0.495-0.443 0.296-0.719l-0.905-1.26 0.113-0.375 1.453-0.548c0.324-0.119 0.401-0.531 0.157-0.76l-1.141-1.068c0.011-0.124 0.027-0.255 0.036-0.385l1.329-0.823c0.291-0.172 0.291-0.599 0-0.771zM22.703 26.615c-1.224-0.26-0.833-2.099 0.396-1.839 0.505 0.109 0.828 0.609 0.719 1.12-0.109 0.505-0.609 0.828-1.115 0.719zM22.255 23.568c-0.463-0.099-0.921 0.197-1.015 0.661l-0.475 2.192c-1.453 0.661-3.068 1.032-4.765 1.032-1.683 0-3.344-0.365-4.864-1.073l-0.475-2.197c-0.099-0.464-0.552-0.761-1.016-0.656l-1.937 0.411c-0.364-0.369-0.697-0.765-1.005-1.183h9.443c0.105 0 0.177-0.015 0.177-0.115v-3.337c0-0.1-0.072-0.115-0.177-0.115h-2.765v-2.12h2.989c0.271 0 1.459 0.077 1.833 1.593 0.12 0.464 0.381 1.979 0.557 2.464 0.177 0.547 0.901 1.629 1.672 1.629h4.704c0.057 0 0.115-0.005 0.171-0.015-0.323 0.443-0.681 0.864-1.067 1.255zM9.197 26.573c-0.509 0.104-1.009-0.219-1.119-0.724-0.109-0.511 0.213-1.011 0.724-1.115 0.504-0.109 1.004 0.213 1.115 0.719 0.109 0.511-0.215 1.011-0.72 1.12zM5.615 12.047c0.229 0.48 0.021 1.052-0.464 1.271-0.484 0.213-1.052-0.015-1.255-0.505-0.459-1.124 1.192-1.859 1.719-0.765zM4.516 14.656l2.020-0.896c0.433-0.192 0.625-0.697 0.433-1.129l-0.417-0.943h1.641v7.38h-3.303c-0.407-1.432-0.536-2.932-0.375-4.412zM13.385 13.943v-2.177h3.896c0.203 0 1.421 0.235 1.421 1.147 0 0.76-0.937 1.031-1.703 1.031zM27.552 15.901c0 0.287-0.011 0.572-0.031 0.853h-1.188c-0.12 0-0.167 0.079-0.167 0.199v0.541c0 1.281-0.719 1.557-1.355 1.631-0.604 0.068-1.271-0.251-1.353-0.62-0.355-2-0.948-2.427-1.885-3.167 1.161-0.74 2.369-1.823 2.369-3.281 0-1.573-1.079-2.563-1.812-3.047-1.032-0.683-2.172-0.817-2.48-0.817h-12.255c1.692-1.896 3.964-3.177 6.459-3.652l1.448 1.521c0.323 0.339 0.869 0.353 1.208 0.025l1.615-1.547c3.339 0.625 6.229 2.683 7.911 5.631l-1.109 2.5c-0.187 0.432 0.005 0.937 0.437 1.131l2.131 0.947c0.036 0.381 0.052 0.761 0.052 1.152zM15.303 3.255c0.905-0.864 2.203 0.495 1.296 1.36-0.901 0.781-2.115-0.495-1.296-1.36zM26.287 12.093c0.208-0.473 0.76-0.687 1.235-0.479 0.771 0.339 0.744 1.437-0.037 1.74-0.787 0.301-1.541-0.495-1.197-1.261z" />
114114
</svg>
115115
);
116+
case "dart":
117+
return (
118+
<svg
119+
className={props.className}
120+
fill="currentColor"
121+
viewBox="0 0 24 24"
122+
>
123+
<path d="M14.314 0L2.3 12 0 14.3 3.5 21.6 8.3 24 24 8.3 14.314 0zm0 3.328l6.417 6.417L8.88 21.594l-3.328-1.428L3.328 14.314l8.88-8.88 2.106-2.106z" />
124+
</svg>
125+
);
116126
default:
117127
props.lang satisfies never;
118128
console.warn("unknown lang for LanguageIcon:", props.lang);

‎app/terminal/page.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import sub_h from "./samples/sub.h?raw";
2626
import sub_cpp from "./samples/sub.cpp?raw";
2727
import main2_rs from "./samples/main2.rs?raw";
2828
import sub_rs from "./samples/sub.rs?raw";
29+
import main_dart from "./samples/main.dart?raw";
2930
import { DaisyInfoIcon } from "@/daisyAlertIcon";
3031

3132
export default function RuntimeTestPage() {
@@ -127,6 +128,13 @@ const sampleConfig: Record<RuntimeLang, SampleConfig> = {
127128
},
128129
exec: ["main2.rs"],
129130
},
131+
dart: {
132+
repl: false,
133+
editor: {
134+
"main.dart": main_dart,
135+
},
136+
exec: ["main.dart"],
137+
},
130138
};
131139
function RuntimeSample({
132140
lang,

‎app/terminal/samples/main.dart‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
void main() {
2+
print("Hello, Dart!");
3+
}

‎packages/runtime/src/context.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { ReactNode, useEffect } from "react";
44
import { RuntimeContext } from "./interface";
55
import { RuntimeLang } from "./languages";
6+
import { DartProvider, useDart } from "./dart/runtime";
67
import { TypeScriptProvider, useTypeScript } from "./typescript/runtime";
78
import { useWandbox, WandboxProvider } from "./wandbox/runtime";
89
import { JSEvalContext, useJSEval } from "./worker/jsEval";
@@ -34,6 +35,7 @@ export function useRuntimeAll(): Record<RuntimeLang, RuntimeContext> {
3435
const typescript = useTypeScript(jsEval);
3536
const wandboxCpp = useWandbox("cpp");
3637
const wandboxRust = useWandbox("rust");
38+
const dart = useDart();
3739

3840
// initはしない。呼び出し側でする必要がある
3941
return {
@@ -43,6 +45,7 @@ export function useRuntimeAll(): Record<RuntimeLang, RuntimeContext> {
4345
typescript: typescript,
4446
cpp: wandboxCpp,
4547
rust: wandboxRust,
48+
dart: dart,
4649
};
4750
}
4851
export function RuntimeProvider({ children }: { children: ReactNode }) {
@@ -51,10 +54,13 @@ export function RuntimeProvider({ children }: { children: ReactNode }) {
5154
<WorkerProvider context={RubyContext} lang="ruby">
5255
<WorkerProvider context={JSEvalContext} lang="javascript">
5356
<WandboxProvider>
54-
<TypeScriptProvider>{children}</TypeScriptProvider>
57+
<TypeScriptProvider>
58+
<DartProvider>{children}</DartProvider>
59+
</TypeScriptProvider>
5560
</WandboxProvider>
5661
</WorkerProvider>
5762
</WorkerProvider>
5863
</WorkerProvider>
5964
);
6065
}
66+
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
"use client";
2+
3+
import {
4+
createContext,
5+
ReactNode,
6+
useCallback,
7+
useContext,
8+
useEffect,
9+
useMemo,
10+
useRef,
11+
} from "react";
12+
import useSWR from "swr";
13+
import {
14+
ReplOutput,
15+
RuntimeContext,
16+
RuntimeErrorHandler,
17+
RuntimeInfo,
18+
UpdatedFile,
19+
} from "../interface";
20+
21+
const DART_PAD_API_BASE = "https://stable.api.dartpad.dev/api/v3";
22+
const DART_PAD_ARTIFACTS_BASE = "https://stable.api.dartpad.dev/artifacts";
23+
24+
interface DartVersionResponse {
25+
dartVersion?: string;
26+
flutterVersion?: string;
27+
}
28+
29+
const versionFetcher = async (url: string): Promise<DartVersionResponse> => {
30+
const res = await fetch(url);
31+
if (!res.ok) {
32+
throw new Error(`Failed to fetch Dart version: ${res.statusText}`);
33+
}
34+
return res.json();
35+
};
36+
37+
const DartContext = createContext<{
38+
init: (onError?: RuntimeErrorHandler) => void;
39+
ready: boolean;
40+
dartVersion?: string;
41+
}>({
42+
init: () => undefined,
43+
ready: true,
44+
});
45+
46+
export function DartProvider({ children }: { children: ReactNode }) {
47+
const onErrorRef = useRef<RuntimeErrorHandler | undefined>(undefined);
48+
const init = useCallback((onError?: RuntimeErrorHandler) => {
49+
onErrorRef.current = onError;
50+
}, []);
51+
52+
const { data, error } = useSWR<DartVersionResponse>(
53+
`${DART_PAD_API_BASE}/version`,
54+
versionFetcher
55+
);
56+
57+
useEffect(() => {
58+
if (error) {
59+
console.error("Failed to fetch Dart version info:", error);
60+
onErrorRef.current?.(error);
61+
}
62+
}, [error]);
63+
64+
return (
65+
<DartContext.Provider
66+
value={{
67+
init,
68+
ready: true,
69+
dartVersion: data?.dartVersion,
70+
}}
71+
>
72+
{children}
73+
</DartContext.Provider>
74+
);
75+
}
76+
77+
export function useDart(): RuntimeContext {
78+
const { init: dartInit, ready, dartVersion } = useContext(DartContext);
79+
const onErrorRef = useRef<RuntimeErrorHandler | undefined>(undefined);
80+
81+
const init = useCallback(
82+
(onError?: RuntimeErrorHandler) => {
83+
onErrorRef.current = onError;
84+
dartInit(onError);
85+
},
86+
[dartInit]
87+
);
88+
89+
const runFiles = useCallback(
90+
async (
91+
filenames: string[],
92+
files: Readonly<Record<string, string>>,
93+
onOutput: (output: ReplOutput | UpdatedFile) => void
94+
) => {
95+
if (typeof window === "undefined") {
96+
onOutput({
97+
type: "error",
98+
message: "Dart runtime requires browser environment.",
99+
});
100+
return;
101+
}
102+
103+
const filename = filenames[0] ?? Object.keys(files)[0];
104+
const source = files[filename] ?? Object.values(files)[0];
105+
106+
if (!source) {
107+
onOutput({ type: "error", message: "No source code provided to run." });
108+
return;
109+
}
110+
111+
try {
112+
const response = await fetch(`${DART_PAD_API_BASE}/compileNewDDC`, {
113+
method: "POST",
114+
headers: {
115+
"Content-Type": "application/json",
116+
},
117+
body: JSON.stringify({ source }),
118+
});
119+
120+
if (!response.ok) {
121+
const errorText = await response.text();
122+
onOutput({
123+
type: "error",
124+
message:
125+
errorText ||
126+
`Compilation failed with status ${response.status}`,
127+
});
128+
return;
129+
}
130+
131+
const data = await response.json();
132+
if (!data.result) {
133+
onOutput({
134+
type: "error",
135+
message: "Compilation returned empty result.",
136+
});
137+
return;
138+
}
139+
140+
const jsCode: string = data.result;
141+
142+
// Execute compiled JS inside a temporary iframe
143+
await new Promise<void>((resolve) => {
144+
const iframe = document.createElement("iframe");
145+
iframe.style.display = "none";
146+
document.body.appendChild(iframe);
147+
148+
let resolved = false;
149+
const cleanup = () => {
150+
if (resolved) return;
151+
resolved = true;
152+
window.removeEventListener("message", handleMessage);
153+
if (iframe.parentNode) {
154+
iframe.parentNode.removeChild(iframe);
155+
}
156+
resolve();
157+
};
158+
159+
const handleMessage = (event: MessageEvent) => {
160+
if (event.source !== iframe.contentWindow) return;
161+
const msgData = event.data;
162+
if (!msgData || msgData.sender !== "dart_frame") return;
163+
164+
if (msgData.type === "stdout") {
165+
onOutput({ type: "stdout", message: String(msgData.message) });
166+
} else if (msgData.type === "stderr") {
167+
onOutput({ type: "stderr", message: String(msgData.message) });
168+
} else if (msgData.type === "done") {
169+
cleanup();
170+
} else if (msgData.type === "error") {
171+
onOutput({ type: "error", message: String(msgData.message) });
172+
cleanup();
173+
}
174+
};
175+
176+
window.addEventListener("message", handleMessage);
177+
178+
const iframeDoc = iframe.contentDocument;
179+
if (!iframeDoc) {
180+
onOutput({
181+
type: "error",
182+
message: "Failed to access iframe document.",
183+
});
184+
cleanup();
185+
return;
186+
}
187+
188+
const htmlContent = `<!DOCTYPE html>
189+
<html>
190+
<head>
191+
<script src="https://dartpad.dev/require.js"></script>
192+
</head>
193+
<body>
194+
<script>
195+
window.dartPrint = function(message) {
196+
window.parent.postMessage({
197+
sender: 'dart_frame',
198+
type: 'stdout',
199+
message: message == null ? '' : message.toString()
200+
}, '*');
201+
};
202+
203+
window.onerror = function(message, url, line, column, error) {
204+
var msg = message;
205+
if (error) {
206+
var errStr = String(error);
207+
var errStack = error.stack || '';
208+
msg = errStr + (errStack ? '\\n' + errStack : '');
209+
}
210+
window.parent.postMessage({
211+
sender: 'dart_frame',
212+
type: 'stderr',
213+
message: msg
214+
}, '*');
215+
};
216+
217+
require.config({
218+
baseUrl: "${DART_PAD_ARTIFACTS_BASE}/",
219+
waitSeconds: 60,
220+
onNodeCreated: function(node, config, id, url) {
221+
node.setAttribute('crossorigin', 'anonymous');
222+
}
223+
});
224+
225+
{
226+
let __ddcInitCode = function() {
227+
${jsCode}
228+
};
229+
230+
function contextLoaded() {
231+
try {
232+
__ddcInitCode();
233+
dartDevEmbedder.runMain('package:dartpad_sample/bootstrap.dart', {});
234+
} catch (err) {
235+
var msg = String(err) + (err && err.stack ? '\\n' + err.stack : '');
236+
window.parent.postMessage({
237+
sender: 'dart_frame',
238+
type: 'stderr',
239+
message: msg
240+
}, '*');
241+
} finally {
242+
setTimeout(function() {
243+
window.parent.postMessage({
244+
sender: 'dart_frame',
245+
type: 'done'
246+
}, '*');
247+
}, 100);
248+
}
249+
}
250+
251+
function moduleLoaderLoaded() {
252+
require(["dart_sdk_new"], contextLoaded);
253+
}
254+
255+
require(["ddc_module_loader"], moduleLoaderLoaded);
256+
}
257+
</script>
258+
</body>
259+
</html>`;
260+
261+
iframeDoc.open();
262+
iframeDoc.write(htmlContent);
263+
iframeDoc.close();
264+
265+
setTimeout(() => {
266+
cleanup();
267+
}, 15000);
268+
});
269+
} catch (error) {
270+
onErrorRef.current?.(error);
271+
onOutput({
272+
type: "fatalError",
273+
message: error instanceof Error ? error.message : String(error),
274+
});
275+
}
276+
},
277+
[]
278+
);
279+
280+
const runtimeInfo = useMemo<RuntimeInfo>(
281+
() => ({
282+
prettyLangName: "Dart",
283+
version: dartVersion,
284+
}),
285+
[dartVersion]
286+
);
287+
288+
return {
289+
init,
290+
ready,
291+
runFiles,
292+
getCommandlineStr,
293+
runtimeInfo,
294+
};
295+
}
296+
297+
function getCommandlineStr(filenames: string[]) {
298+
return `dart run ${filenames[0] ?? "main.dart"}`;
299+
}

0 commit comments

Comments
 (0)