diff --git a/.gitignore b/.gitignore index efb8a61..181e30d 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + + +/public/typescript/ diff --git a/app/[docs_id]/markdown.tsx b/app/[docs_id]/markdown.tsx index b3a8c03..cf8ad75 100644 --- a/app/[docs_id]/markdown.tsx +++ b/app/[docs_id]/markdown.tsx @@ -1,6 +1,5 @@ import Markdown, { Components } from "react-markdown"; import remarkGfm from "remark-gfm"; -import SyntaxHighlighter from "react-syntax-highlighter"; import { EditorComponent, getAceLang } from "../terminal/editor"; import { ExecFile } from "../terminal/exec"; import { useChangeTheme } from "./themeToggle"; @@ -11,6 +10,9 @@ import { import { ReactNode } from "react"; import { getRuntimeLang } from "@/terminal/runtime"; import { ReplTerminal } from "@/terminal/repl"; +import dynamic from "next/dynamic"; +// SyntaxHighlighterはファイルサイズがでかいので & HydrationErrorを起こすので、SSRを無効化する +const SyntaxHighlighter = dynamic(() => import("react-syntax-highlighter"), { ssr: false }); export function StyledMarkdown({ content }: { content: string }) { return ( diff --git a/app/terminal/README.md b/app/terminal/README.md index fd4329c..ec89b6e 100644 --- a/app/terminal/README.md +++ b/app/terminal/README.md @@ -51,8 +51,9 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ ### ファイル実行用 -* runFiles: `(filenames: string[]) => Promise` - * 指定されたファイルを実行します。ファイルの中身はEmbedContextから取得されます。 +* runFiles: `(filenames: string[], files: Record) => Promise` + * 指定されたファイルを実行します。 + * EmbedContextから取得したfilesを呼び出し側で引数に渡します * 呼び出し側でmutexのロックはせず、必要であればrunFiles()内でロックします。 * getCommandlineStr: `(filenames: string[]) => string` * 指定されたファイルを実行するためのコマンドライン引数文字列を返します。表示用です。 diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index 386587d..70ebd3f 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -14,6 +14,7 @@ const AceEditor = dynamic( await import("ace-builds/src-min-noconflict/mode-ruby"); await import("ace-builds/src-min-noconflict/mode-c_cpp"); await import("ace-builds/src-min-noconflict/mode-javascript"); + await import("ace-builds/src-min-noconflict/mode-typescript"); 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"); @@ -30,7 +31,15 @@ import { langConstants } from "./runtime"; // snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python"; // mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値が対応する -export type AceLang = "python" | "ruby" | "c_cpp" | "javascript" | "json" | "csv" | "text"; +export type AceLang = + | "python" + | "ruby" + | "c_cpp" + | "javascript" + | "typescript" + | "json" + | "csv" + | "text"; export function getAceLang(lang: string | undefined): AceLang { // Markdownで指定される可能性のある言語名からAceLangを取得 switch (lang) { @@ -46,6 +55,9 @@ export function getAceLang(lang: string | undefined): AceLang { case "javascript": case "js": return "javascript"; + case "typescript": + case "ts": + return "typescript"; case "json": return "json"; case "csv": @@ -73,7 +85,7 @@ export function EditorComponent(props: EditorProps) { const code = files[props.filename] || props.initContent; useEffect(() => { if (!files[props.filename]) { - writeFile(props.filename, props.initContent); + writeFile({ [props.filename]: props.initContent }); } }, [files, props.filename, props.initContent, writeFile]); @@ -92,7 +104,7 @@ export function EditorComponent(props: EditorProps) { // codeの内容が変更された場合のみ表示する (props.readonly || code == props.initContent) && "invisible" )} - onClick={() => writeFile(props.filename, props.initContent)} + onClick={() => writeFile({ [props.filename]: props.initContent })} > {/**/} writeFile(props.filename, code)} + onChange={(code: string) => writeFile({ [props.filename]: code })} /> ); diff --git a/app/terminal/embedContext.tsx b/app/terminal/embedContext.tsx index 8749307..53138d3 100644 --- a/app/terminal/embedContext.tsx +++ b/app/terminal/embedContext.tsx @@ -24,17 +24,21 @@ type Filename = string; type TerminalId = string; interface IEmbedContext { - files: Record; - writeFile: (name: Filename, content: string) => void; + files: Readonly>; + // ファイルを書き込む。更新後のページ内の全ファイル内容を返す + // 返り値を使うことで再レンダリングを待たずに最新の内容を取得できる + writeFile: ( + updatedFiles: Readonly> + ) => Promise>>; - replOutputs: Record; + replOutputs: Readonly>; addReplOutput: ( terminalId: TerminalId, command: string, output: ReplOutput[] ) => void; - execResults: Record; + execResults: Readonly>; setExecResult: (filename: Filename, output: ReplOutput[]) => void; } const EmbedContext = createContext(null!); @@ -72,16 +76,26 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) { }, [pathname, currentPathname]); const writeFile = useCallback( - (name: Filename, content: string) => { - setFiles((files) => { - if (files[pathname]?.[name] !== content) { - files = { ...files }; - files[pathname] = { ...(files[pathname] ?? {}) }; - files[pathname][name] = content; - return files; - } else { - return files; - } + (updatedFiles: Record) => { + return new Promise>((resolve) => { + setFiles((files) => { + let changed = false; + const newFiles = { ...files }; + newFiles[pathname] = { ...(newFiles[pathname] ?? {}) }; + for (const [name, content] of Object.entries(updatedFiles)) { + if (newFiles[pathname][name] !== content) { + changed = true; + newFiles[pathname][name] = content; + } + } + if (changed) { + resolve(newFiles[pathname]); + return newFiles; + } else { + resolve(files[pathname] || {}); + return files; + } + }); }); }, [pathname] diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index 8b98bbe..73c1448 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -31,7 +31,7 @@ export function ExecFile(props: ExecProps) { } }, }); - const { setExecResult } = useEmbedContext(); + const { files, setExecResult } = useEmbedContext(); const { ready, runFiles, getCommandlineStr } = useRuntime(props.language); @@ -45,13 +45,14 @@ export function ExecFile(props: ExecProps) { (async () => { clearTerminal(terminalInstanceRef.current!); terminalInstanceRef.current!.write(systemMessageColor("実行中です...")); - const outputs = await runFiles(props.filenames); + const outputs = await runFiles(props.filenames, files); clearTerminal(terminalInstanceRef.current!); writeOutput( terminalInstanceRef.current!, outputs, false, undefined, + null, // ファイル実行で"return"メッセージが返ってくることはないはずなので、Prismを渡す必要はない props.language ); // TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる @@ -67,6 +68,7 @@ export function ExecFile(props: ExecProps) { setExecResult, terminalInstanceRef, props.language, + files, ]); return ( diff --git a/app/terminal/highlight.ts b/app/terminal/highlight.ts index 5bc7069..76a1dc1 100644 --- a/app/terminal/highlight.ts +++ b/app/terminal/highlight.ts @@ -1,10 +1,18 @@ -import Prism from "prismjs"; import chalk from "chalk"; import { RuntimeLang } from "./runtime"; -// 言語定義をインポート -import "prismjs/components/prism-python"; -import "prismjs/components/prism-ruby"; -import "prismjs/components/prism-javascript"; + +export async function importPrism() { + if (typeof window !== "undefined") { + const Prism = await import("prismjs"); + // 言語定義をインポート + await import("prismjs/components/prism-python"); + await import("prismjs/components/prism-ruby"); + await import("prismjs/components/prism-javascript"); + return Prism; + } else { + return null!; + } +} type PrismLang = "python" | "ruby" | "javascript"; @@ -17,6 +25,7 @@ function getPrismLanguage(language: RuntimeLang): PrismLang { case "javascript": return "javascript"; case "cpp": + case "typescript": throw new Error( `highlight for ${language} is disabled because it should not support REPL` ); @@ -74,6 +83,7 @@ const prismToAnsi: Record string> = { * @returns {string} ANSIで色付けされた文字列 */ export function highlightCodeToAnsi( + Prism: typeof import("prismjs"), code: string, language: RuntimeLang ): string { @@ -129,3 +139,4 @@ export function highlightCodeToAnsi( "" ); } + diff --git a/app/terminal/page.tsx b/app/terminal/page.tsx index 8b30101..3125790 100644 --- a/app/terminal/page.tsx +++ b/app/terminal/page.tsx @@ -1,6 +1,5 @@ "use client"; import { Heading } from "@/[docs_id]/markdown"; -import "mocha/mocha.js"; import "mocha/mocha.css"; import { Fragment, useEffect, useRef, useState } from "react"; import { useWandbox } from "./wandbox/runtime"; @@ -13,6 +12,7 @@ import { useJSEval } from "./worker/jsEval"; import { ReplTerminal } from "./repl"; import { EditorComponent, getAceLang } from "./editor"; import { ExecFile } from "./exec"; +import { useTypeScript } from "./typescript/runtime"; export default function RuntimeTestPage() { return ( @@ -69,8 +69,17 @@ const sampleConfig: Record = { javascript: { repl: true, replInitContent: '> console.log("Hello, World!");\nHello, World!', - editor: false, - exec: false, + editor: { + "main.js": 'console.log("Hello, World!");', + }, + exec: ["main.js"], + }, + typescript: { + repl: false, + editor: { + "main.ts": 'function greet(name: string): void {\n console.log("Hello, " + name + "!");\n}\n\ngreet("World");', + }, + exec: ["main.ts"], }, cpp: { repl: false, @@ -122,13 +131,15 @@ function RuntimeSample({ function MochaTest() { const pyodide = usePyodide(); const ruby = useRuby(); - const javascript = useJSEval(); + const jsEval = useJSEval(); + const typescript = useTypeScript(jsEval); const wandboxCpp = useWandbox("cpp"); const runtimeRef = useRef>(null!); runtimeRef.current = { python: pyodide, ruby: ruby, - javascript: javascript, + javascript: jsEval, + typescript: typescript, cpp: wandboxCpp, }; @@ -139,21 +150,27 @@ function MochaTest() { const [mochaState, setMochaState] = useState<"idle" | "running" | "finished">( "idle" ); - const { writeFile } = useEmbedContext(); + const { files } = useEmbedContext(); + const filesRef = useRef(files); + filesRef.current = files; - const runTest = () => { - setMochaState("running"); + const runTest = async () => { + if(typeof window !== "undefined") { + setMochaState("running"); + + await import("mocha/mocha.js"); - mocha.setup("bdd"); + mocha.setup("bdd"); - for (const lang of Object.keys(runtimeRef.current) as RuntimeLang[]) { - defineTests(lang, runtimeRef, writeFile); - } + for (const lang of Object.keys(runtimeRef.current) as RuntimeLang[]) { + defineTests(lang, runtimeRef, filesRef); + } - const runner = mocha.run(); - runner.on("end", () => { - setMochaState("finished"); - }); + const runner = mocha.run(); + runner.on("end", () => { + setMochaState("finished"); + }); + } }; return ( diff --git a/app/terminal/repl.tsx b/app/terminal/repl.tsx index 3e16fb1..79666ef 100644 --- a/app/terminal/repl.tsx +++ b/app/terminal/repl.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; -import { highlightCodeToAnsi } from "./highlight"; +import { highlightCodeToAnsi, importPrism } from "./highlight"; import chalk from "chalk"; import { clearTerminal, @@ -11,7 +11,7 @@ import { systemMessageColor, useTerminal, } from "./terminal"; -import { Terminal } from "@xterm/xterm"; +import type { Terminal } from "@xterm/xterm"; import { useEmbedContext } from "./embedContext"; import { emptyMutex, langConstants, RuntimeLang, useRuntime } from "./runtime"; @@ -30,6 +30,7 @@ export function writeOutput( outputs: ReplOutput[], endNewLine: boolean, returnPrefix: string | undefined, + Prism: typeof import("prismjs") | null, language: RuntimeLang ) { for (let i = 0; i < outputs.length; i++) { @@ -53,7 +54,12 @@ export function writeOutput( if (returnPrefix) { term.write(returnPrefix); } - term.write(highlightCodeToAnsi(message, language)); + if (Prism) { + term.write(highlightCodeToAnsi(Prism, message, language)); + } else { + console.warn("Prism is not loaded, cannot highlight return value"); + term.write(message); + } break; default: term.write(message); @@ -77,6 +83,13 @@ export function ReplTerminal({ }: ReplComponentProps) { const { addReplOutput } = useEmbedContext(); + const [Prism, setPrism] = useState(null); + useEffect(() => { + if (Prism === null) { + importPrism().then((prism) => setPrism(prism)); + } + }, [Prism]); + const { ready: runtimeReady, mutex: runtimeMutex = emptyMutex, @@ -130,7 +143,7 @@ export function ReplTerminal({ // inputBufferを更新し、画面に描画する const updateBuffer = useCallback( (newBuffer: () => string[]) => { - if (terminalInstanceRef.current) { + if (terminalInstanceRef.current && Prism) { hideCursor(terminalInstanceRef.current); // バッファの行数分カーソルを戻す if (inputBuffer.current.length >= 2) { @@ -149,7 +162,7 @@ export function ReplTerminal({ ); if (language) { terminalInstanceRef.current.write( - highlightCodeToAnsi(inputBuffer.current[i], language) + highlightCodeToAnsi(Prism, inputBuffer.current[i], language) ); } else { terminalInstanceRef.current.write(inputBuffer.current[i]); @@ -161,7 +174,7 @@ export function ReplTerminal({ showCursor(terminalInstanceRef.current); } }, - [prompt, promptMore, language, terminalInstanceRef] + [Prism, prompt, promptMore, language, terminalInstanceRef] ); // ランタイムからのoutputを描画し、inputBufferをリセット @@ -173,6 +186,7 @@ export function ReplTerminal({ outputs, true, returnPrefix, + Prism, language ); // 出力が終わったらプロンプトを表示 diff --git a/app/terminal/runtime.tsx b/app/terminal/runtime.tsx index 520c244..2778ca6 100644 --- a/app/terminal/runtime.tsx +++ b/app/terminal/runtime.tsx @@ -7,6 +7,7 @@ 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"; /** * Common runtime context interface for different languages @@ -23,7 +24,7 @@ export interface RuntimeContext { checkSyntax?: (code: string) => Promise; splitReplExamples?: (content: string) => ReplCommand[]; // file - runFiles: (filenames: string[]) => Promise; + runFiles: (filenames: string[], files: Readonly>) => Promise; getCommandlineStr?: (filenames: string[]) => string; } export interface LangConstants { @@ -32,7 +33,12 @@ export interface LangConstants { promptMore?: string; returnPrefix?: string; } -export type RuntimeLang = "python" | "ruby" | "cpp" | "javascript"; +export type RuntimeLang = + | "python" + | "ruby" + | "cpp" + | "javascript" + | "typescript"; export function getRuntimeLang( lang: string | undefined @@ -51,6 +57,9 @@ export function getRuntimeLang( case "javascript": case "js": return "javascript"; + case "typescript": + case "ts": + return "typescript"; default: console.warn(`Unsupported language for runtime: ${lang}`); return undefined; @@ -61,6 +70,7 @@ export function useRuntime(language: RuntimeLang): RuntimeContext { const pyodide = usePyodide(); const ruby = useRuby(); const jsEval = useJSEval(); + const typescript = useTypeScript(jsEval); const wandboxCpp = useWandbox("cpp"); switch (language) { @@ -70,6 +80,8 @@ export function useRuntime(language: RuntimeLang): RuntimeContext { return ruby; case "javascript": return jsEval; + case "typescript": + return typescript; case "cpp": return wandboxCpp; default: @@ -82,7 +94,9 @@ export function RuntimeProvider({ children }: { children: ReactNode }) { - {children} + + {children} + @@ -106,6 +120,7 @@ export function langConstants(lang: RuntimeLang | AceLang): LangConstants { returnPrefix: "=> ", }; case "javascript": + case "typescript": return { tabSize: 2, prompt: "> ", diff --git a/app/terminal/terminal.tsx b/app/terminal/terminal.tsx index aaf2b8a..0ce015b 100644 --- a/app/terminal/terminal.tsx +++ b/app/terminal/terminal.tsx @@ -1,8 +1,8 @@ "use client"; import { useEffect, useRef, useState } from "react"; -import { Terminal } from "@xterm/xterm"; -import { FitAddon } from "@xterm/addon-fit"; +import type { Terminal } from "@xterm/xterm"; +import type { FitAddon } from "@xterm/addon-fit"; import "@xterm/xterm/css/xterm.css"; import chalk from "chalk"; import { useChangeTheme } from "../[docs_id]/themeToggle"; @@ -70,104 +70,110 @@ export function useTerminal(props: TerminalProps) { // ターミナルの初期化処理 useEffect(() => { - const abortController = new AbortController(); - // globals.cssでフォントを指定し読み込んでいるが、 - // それが読み込まれる前にterminalを初期化してしまうとバグる。 - document.fonts.load("0.875rem Inconsolata Variable").then(() => { - if (!abortController.signal.aborted) { - const fromCSS = (varName: string) => - window.getComputedStyle(document.body).getPropertyValue(varName); - // "--color-" + color_name のように文字列を分割するとTailwindCSSが認識せずCSSの値として出力されない場合があるので注意 - const term = new Terminal({ - cursorBlink: true, - convertEol: true, - cursorStyle: "bar", - cursorInactiveStyle: "none", - fontSize: 14, - lineHeight: 1.4, - letterSpacing: 0, - fontFamily: "'Inconsolata Variable','Noto Sans JP Variable'", - theme: { - // DaisyUIの変数を使用してテーマを設定している - // TODO: ダークテーマ/ライトテーマを切り替えたときに再設定する? - background: fromCSS("--color-base-300"), - foreground: fromCSS("--color-base-content"), - cursor: fromCSS("--color-base-content"), - selectionBackground: fromCSS("--color-primary"), - selectionForeground: fromCSS("--color-primary-content"), - black: fromCSS("--color-black"), - brightBlack: fromCSS("--color-neutral-500"), - red: fromCSS("--color-red-600"), - brightRed: fromCSS("--color-red-400"), - green: fromCSS("--color-green-600"), - brightGreen: fromCSS("--color-green-400"), - yellow: fromCSS("--color-yellow-700"), - brightYellow: fromCSS("--color-yellow-400"), - blue: fromCSS("--color-indigo-600"), - brightBlue: fromCSS("--color-indigo-400"), - magenta: fromCSS("--color-fuchsia-600"), - brightMagenta: fromCSS("--color-fuchsia-400"), - cyan: fromCSS("--color-cyan-600"), - brightCyan: fromCSS("--color-cyan-400"), - white: fromCSS("--color-base-100"), - brightWhite: fromCSS("--color-white"), - }, - }); - terminalInstanceRef.current = term; - - fitAddonRef.current = new FitAddon(); - term.loadAddon(fitAddonRef.current); - // fitAddon.fit(); - - term.open(terminalRef.current); - - // https://github.com/xtermjs/xterm.js/issues/2478 - // my.code();ではCtrl+Cでのkeyboardinterruptは要らないので、コピーペーストに置き換えてしまう - term.attachCustomKeyEventHandler((arg) => { - if ( - arg.ctrlKey && - (arg.key === "c" || arg.key === "x") && - arg.type === "keydown" - ) { - const selection = term.getSelection(); - if (selection) { - navigator.clipboard.writeText(selection); + if (typeof window !== "undefined") { + const abortController = new AbortController(); + // globals.cssでフォントを指定し読み込んでいるが、 + // それが読み込まれる前にterminalを初期化してしまうとバグる。 + Promise.all([ + import("@xterm/xterm"), + import("@xterm/addon-fit"), + document.fonts.load("0.875rem Inconsolata Variable"), + ]).then(([{ Terminal }, { FitAddon }]) => { + if (!abortController.signal.aborted) { + const fromCSS = (varName: string) => + window.getComputedStyle(document.body).getPropertyValue(varName); + // "--color-" + color_name のように文字列を分割するとTailwindCSSが認識せずCSSの値として出力されない場合があるので注意 + const term = new Terminal({ + cursorBlink: true, + convertEol: true, + cursorStyle: "bar", + cursorInactiveStyle: "none", + fontSize: 14, + lineHeight: 1.4, + letterSpacing: 0, + fontFamily: "'Inconsolata Variable','Noto Sans JP Variable'", + theme: { + // DaisyUIの変数を使用してテーマを設定している + // TODO: ダークテーマ/ライトテーマを切り替えたときに再設定する? + background: fromCSS("--color-base-300"), + foreground: fromCSS("--color-base-content"), + cursor: fromCSS("--color-base-content"), + selectionBackground: fromCSS("--color-primary"), + selectionForeground: fromCSS("--color-primary-content"), + black: fromCSS("--color-black"), + brightBlack: fromCSS("--color-neutral-500"), + red: fromCSS("--color-red-600"), + brightRed: fromCSS("--color-red-400"), + green: fromCSS("--color-green-600"), + brightGreen: fromCSS("--color-green-400"), + yellow: fromCSS("--color-yellow-700"), + brightYellow: fromCSS("--color-yellow-400"), + blue: fromCSS("--color-indigo-600"), + brightBlue: fromCSS("--color-indigo-400"), + magenta: fromCSS("--color-fuchsia-600"), + brightMagenta: fromCSS("--color-fuchsia-400"), + cyan: fromCSS("--color-cyan-600"), + brightCyan: fromCSS("--color-cyan-400"), + white: fromCSS("--color-base-100"), + brightWhite: fromCSS("--color-white"), + }, + }); + terminalInstanceRef.current = term; + + fitAddonRef.current = new FitAddon(); + term.loadAddon(fitAddonRef.current); + // fitAddon.fit(); + + term.open(terminalRef.current); + + // https://github.com/xtermjs/xterm.js/issues/2478 + // my.code();ではCtrl+Cでのkeyboardinterruptは要らないので、コピーペーストに置き換えてしまう + term.attachCustomKeyEventHandler((arg) => { + if ( + arg.ctrlKey && + (arg.key === "c" || arg.key === "x") && + arg.type === "keydown" + ) { + const selection = term.getSelection(); + if (selection) { + navigator.clipboard.writeText(selection); + return false; + } + } + if (arg.ctrlKey && arg.key === "v" && arg.type === "keydown") { return false; } - } - if (arg.ctrlKey && arg.key === "v" && arg.type === "keydown") { - return false; - } - return true; - }); - - setTermReady(true); - onReadyRef.current?.(); - } - }); - - const observer = new ResizeObserver(() => { - // fitAddon.fit(); - const dims = fitAddonRef.current?.proposeDimensions(); - if (dims && !isNaN(dims.cols)) { - const rows = Math.max(5, getRowsRef.current?.(dims.cols) ?? 0); - terminalInstanceRef.current?.resize(dims.cols, rows); - } - }); - observer.observe(terminalRef.current); - - return () => { - abortController.abort("terminal component dismount"); - observer.disconnect(); - if (fitAddonRef.current) { - fitAddonRef.current.dispose(); - fitAddonRef.current = null; - } - if (terminalInstanceRef.current) { - terminalInstanceRef.current.dispose(); - terminalInstanceRef.current = null; - } - }; + return true; + }); + + setTermReady(true); + onReadyRef.current?.(); + } + }); + + const observer = new ResizeObserver(() => { + // fitAddon.fit(); + const dims = fitAddonRef.current?.proposeDimensions(); + if (dims && !isNaN(dims.cols)) { + const rows = Math.max(5, getRowsRef.current?.(dims.cols) ?? 0); + terminalInstanceRef.current?.resize(dims.cols, rows); + } + }); + observer.observe(terminalRef.current); + + return () => { + abortController.abort("terminal component dismount"); + observer.disconnect(); + if (fitAddonRef.current) { + fitAddonRef.current.dispose(); + fitAddonRef.current = null; + } + if (terminalInstanceRef.current) { + terminalInstanceRef.current.dispose(); + terminalInstanceRef.current = null; + } + }; + } }, []); // テーマが変わったときにterminalのテーマを更新する diff --git a/app/terminal/tests.ts b/app/terminal/tests.ts index 52fbc70..308629b 100644 --- a/app/terminal/tests.ts +++ b/app/terminal/tests.ts @@ -5,7 +5,7 @@ import { emptyMutex, RuntimeContext, RuntimeLang } from "./runtime"; export function defineTests( lang: RuntimeLang, runtimeRef: RefObject>, - writeFile: (name: string, content: string) => void + filesRef: RefObject>> ) { describe(`${lang} Runtime`, function () { this.timeout( @@ -35,6 +35,7 @@ export function defineTests( ruby: `puts "${msg}"`, cpp: null, javascript: `console.log("${msg}")`, + typescript: null, } satisfies Record )[lang]; if (!printCode) { @@ -44,12 +45,7 @@ export function defineTests( runtimeRef.current[lang].mutex || emptyMutex ).runExclusive(() => runtimeRef.current[lang].runCommand!(printCode)); console.log(`${lang} REPL stdout test: `, result); - expect(result).to.be.deep.equal([ - { - type: "stdout", - message: msg, - }, - ]); + expect(result).to.be.deep.include({ type: "stdout", message: msg }); }); it("should preserve variables across commands", async function () { @@ -60,7 +56,11 @@ export function defineTests( python: [`${varName} = ${value}`, `print(${varName})`], ruby: [`${varName} = ${value}`, `puts ${varName}`], cpp: [null, null], - javascript: [`var ${varName} = ${value}`, `console.log(${varName})`], + javascript: [ + `var ${varName} = ${value}`, + `console.log(${varName})`, + ], + typescript: [null, null], } satisfies Record )[lang]; if (!setIntVarCode || !printIntVarCode) { @@ -73,12 +73,10 @@ export function defineTests( return runtimeRef.current[lang].runCommand!(printIntVarCode); }); console.log(`${lang} REPL variable preservation test: `, result); - expect(result).to.be.deep.equal([ - { - type: "stdout", - message: value.toString(), - }, - ]); + expect(result).to.be.deep.include({ + type: "stdout", + message: value.toString(), + }); }); it("should capture errors", async function () { @@ -89,6 +87,7 @@ export function defineTests( ruby: `raise "${errorMsg}"`, cpp: null, javascript: `throw new Error("${errorMsg}")`, + typescript: null, } satisfies Record )[lang]; if (!errorCode) { @@ -109,7 +108,12 @@ export function defineTests( python: [`testVar = 42`, `while True:\n pass`, `print(testVar)`], ruby: [`testVar = 42`, `loop do\nend`, `puts testVar`], cpp: [null, null, null], - javascript: [`var testVar = 42`, `while(true) {}`, `console.log(testVar)`], + javascript: [ + `var testVar = 42`, + `while(true) {}`, + `console.log(testVar)`, + ], + typescript: [null, null, null], } satisfies Record )[lang]; if (!setIntVarCode || !infLoopCode || !printIntVarCode) { @@ -129,19 +133,37 @@ export function defineTests( while (!runtimeRef.current[lang].ready) { await new Promise((resolve) => setTimeout(resolve, 100)); } - await new Promise((resolve) => setTimeout(resolve, 100)); const result = await ( runtimeRef.current[lang].mutex || emptyMutex ).runExclusive(() => runtimeRef.current[lang].runCommand!(printIntVarCode) ); console.log(`${lang} REPL interrupt recovery test: `, result); - expect(result).to.be.deep.equal([ + expect(result).to.be.deep.include({ type: "stdout", message: "42" }); + }); + + it("should capture files modified by command", async function () { + const targetFile = "test.txt"; + const msg = "Hello, World!"; + const writeCode = ( { - type: "stdout", - message: "42", - }, - ]); + python: `with open("${targetFile}", "w") as f:\n f.write("${msg}")`, + ruby: `File.open("${targetFile}", "w") {|f| f.write("${msg}") }`, + cpp: null, + javascript: null, + typescript: null, + } satisfies Record + )[lang]; + if (!writeCode) { + this.skip(); + } + const result = await ( + runtimeRef.current[lang].mutex || emptyMutex + ).runExclusive(() => runtimeRef.current[lang].runCommand!(writeCode)); + console.log(`${lang} REPL file modify test: `, result); + // wait for files to be updated + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(filesRef.current[targetFile]).to.equal(msg); }); }); @@ -156,23 +178,18 @@ export function defineTests( "test.cpp", `#include \nint main() {\n std::cout << "${msg}" << std::endl;\n return 0;\n}\n`, ], - javascript: [null, null], + javascript: ["test.js", `console.log("${msg}")`], + typescript: ["test.ts", `console.log("${msg}")`], } satisfies Record )[lang]; if (!filename || !code) { this.skip(); } - writeFile(filename, code); - // use setTimeout to wait for writeFile to propagate. - await new Promise((resolve) => setTimeout(resolve, 100)); - const result = await runtimeRef.current[lang].runFiles([filename]); + const result = await runtimeRef.current[lang].runFiles([filename], { + [filename]: code, + }); console.log(`${lang} single file stdout test: `, result); - expect(result).to.be.deep.equal([ - { - type: "stdout", - message: msg, - }, - ]); + expect(result).to.be.deep.include({ type: "stdout", message: msg }); }); it("should capture errors", async function () { @@ -185,15 +202,17 @@ export function defineTests( "test_error.cpp", `#include \nint main() {\n throw std::runtime_error("${errorMsg}");\n return 0;\n}\n`, ], - javascript: [null, null], + javascript: ["test_error.js", `throw new Error("${errorMsg}");\n`], + // TODO: tscが出す型エラーのテストはできていない + typescript: ["test_error.ts", `throw new Error("${errorMsg}");\n`], } satisfies Record )[lang]; if (!filename || !code) { this.skip(); } - writeFile(filename, code); - await new Promise((resolve) => setTimeout(resolve, 100)); - const result = await runtimeRef.current[lang].runFiles([filename]); + const result = await runtimeRef.current[lang].runFiles([filename], { + [filename]: code, + }); console.log(`${lang} single file error capture test: `, result); // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(result.filter((r) => r.message.includes(errorMsg))).to.not.be @@ -230,23 +249,51 @@ export function defineTests( ["test_multi_main.cpp", "test_multi_sub.cpp"], ], javascript: [null, null], - } satisfies Record, string[]] | [null, null]> + typescript: [null, null], + } satisfies Record< + RuntimeLang, + [Record, string[]] | [null, null] + > )[lang]; if (!codes || !execFiles) { this.skip(); } - for (const [filename, code] of Object.entries(codes)) { - writeFile(filename, code); - } - await new Promise((resolve) => setTimeout(resolve, 100)); - const result = await runtimeRef.current[lang].runFiles(execFiles); + const result = await runtimeRef.current[lang].runFiles( + execFiles, + codes + ); console.log(`${lang} multifile stdout test: `, result); - expect(result).to.be.deep.equal([ + expect(result).to.be.deep.include({ type: "stdout", message: msg }); + }); + + it("should capture files modified by script", async function () { + const targetFile = "test.txt"; + const msg = "Hello, World!"; + const [filename, code] = ( { - type: "stdout", - message: msg, - }, - ]); + python: [ + "test.py", + `with open("${targetFile}", "w") as f:\n f.write("${msg}")`, + ], + ruby: [ + "test.rb", + `File.open("${targetFile}", "w") {|f| f.write("${msg}") }`, + ], + cpp: [null, null], + javascript: [null, null], + typescript: [null, null], + } satisfies Record + )[lang]; + if (!filename || !code) { + this.skip(); + } + const result = await runtimeRef.current[lang].runFiles([filename], { + [filename]: code, + }); + console.log(`${lang} file modify test: `, result); + // wait for files to be updated + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(filesRef.current[targetFile]).to.equal(msg); }); }); }); diff --git a/app/terminal/typescript/runtime.tsx b/app/terminal/typescript/runtime.tsx new file mode 100644 index 0000000..917da82 --- /dev/null +++ b/app/terminal/typescript/runtime.tsx @@ -0,0 +1,150 @@ +"use client"; + +import type { CompilerOptions } from "typescript"; +import type { VirtualTypeScriptEnvironment } from "@typescript/vfs"; +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useState, +} from "react"; +import { useEmbedContext } from "../embedContext"; +import { ReplOutput } from "../repl"; +import { RuntimeContext } from "../runtime"; + +export const compilerOptions: CompilerOptions = {}; + +const TypeScriptContext = createContext( + null +); +export function TypeScriptProvider({ children }: { children: ReactNode }) { + const [tsEnv, setTSEnv] = useState(null); + + useEffect(() => { + // useEffectはサーバーサイドでは実行されないが、 + // typeof window !== "undefined" でガードしないとなぜかesbuildが"typescript"を + // サーバーサイドでのインポート対象とみなしてしまう。 + if (tsEnv === null && typeof window !== "undefined") { + const abortController = new AbortController(); + (async () => { + const ts = await import("typescript"); + const vfs = await import("@typescript/vfs"); + const system = vfs.createSystem(new Map()); + const libFiles = vfs.knownLibFilesForCompilerOptions( + compilerOptions, + ts + ); + const libFileContents = await Promise.all( + libFiles.map(async (libFile) => { + const response = await fetch( + `/typescript/${ts.version}/${libFile}`, + { signal: abortController.signal } + ); + if (response.ok) { + return response.text(); + } else { + return undefined; + } + }) + ); + libFiles.forEach((libFile, index) => { + const content = libFileContents[index]; + if (content !== undefined) { + system.writeFile(`/${libFile}`, content); + } + }); + const env = vfs.createVirtualTypeScriptEnvironment( + system, + [], + ts, + compilerOptions + ); + setTSEnv(env); + })(); + return () => { + abortController.abort(); + }; + } + }, [tsEnv, setTSEnv]); + return ( + + {children} + + ); +} + +export function useTypeScript(jsEval: RuntimeContext): RuntimeContext { + const tsEnv = useContext(TypeScriptContext); + + const { writeFile } = useEmbedContext(); + const runFiles = useCallback( + async (filenames: string[], files: Readonly>) => { + if (tsEnv === null || typeof window === "undefined") { + return [ + { type: "error" as const, message: "TypeScript is not ready yet." }, + ]; + } else { + for (const [filename, content] of Object.entries(files)) { + tsEnv.createFile(filename, content); + } + + const outputs: ReplOutput[] = []; + + const ts = await import("typescript"); + + for (const diagnostic of tsEnv.languageService.getSyntacticDiagnostics( + filenames[0] + )) { + outputs.push({ + type: "error", + message: ts.formatDiagnosticsWithColorAndContext([diagnostic], { + getCurrentDirectory: () => "", + getCanonicalFileName: (f) => f, + getNewLine: () => "\n", + }), + }); + } + + for (const diagnostic of tsEnv.languageService.getSemanticDiagnostics( + filenames[0] + )) { + outputs.push({ + type: "error", + message: ts.formatDiagnosticsWithColorAndContext([diagnostic], { + getCurrentDirectory: () => "", + getCanonicalFileName: (f) => f, + getNewLine: () => "\n", + }), + }); + } + + const emitOutput = tsEnv.languageService.getEmitOutput(filenames[0]); + files = await writeFile( + Object.fromEntries( + emitOutput.outputFiles.map((of) => [of.name, of.text]) + ) + ); + + console.log(emitOutput); + const jsOutputs = jsEval.runFiles( + [emitOutput.outputFiles[0].name], + files + ); + + return outputs.concat(await jsOutputs); + } + }, + [tsEnv, writeFile, jsEval] + ); + return { + ready: tsEnv !== null, + runFiles, + getCommandlineStr, + }; +} + +function getCommandlineStr(filenames: string[]) { + return `tsc ${filenames.join(" ")}`; +} diff --git a/app/terminal/wandbox/runtime.tsx b/app/terminal/wandbox/runtime.tsx index 8111687..aadbd6b 100644 --- a/app/terminal/wandbox/runtime.tsx +++ b/app/terminal/wandbox/runtime.tsx @@ -10,7 +10,6 @@ import { import useSWR from "swr"; import { compilerInfoFetcher, SelectedCompiler } from "./api"; import { cppRunFiles, selectCppCompiler } from "./cpp"; -import { useEmbedContext } from "../embedContext"; import { RuntimeContext, RuntimeLang } from "../runtime"; import { ReplOutput } from "../repl"; @@ -23,13 +22,15 @@ interface IWandboxContext { ) => (filenames: string[]) => string; runFilesWithLang: ( lang: WandboxLang - ) => (filenames: string[]) => Promise; + ) => ( + filenames: string[], + files: Readonly> + ) => Promise; } const WandboxContext = createContext(null!); export function WandboxProvider({ children }: { children: ReactNode }) { - const { files } = useEmbedContext(); const { data: compilerList, error } = useSWR("list", compilerInfoFetcher); if (error) { console.error("Failed to fetch compiler list from Wandbox:", error); @@ -68,21 +69,22 @@ export function WandboxProvider({ children }: { children: ReactNode }) { // Curried function for language-specific file execution const runFilesWithLang = useCallback( - (lang: WandboxLang) => async (filenames: string[]) => { - if (!selectedCompiler) { - return [ - { type: "error" as const, message: "Wandbox is not ready yet." }, - ]; - } - switch (lang) { - case "cpp": - return cppRunFiles(selectedCompiler.cpp, files, filenames); - default: - lang satisfies never; - throw new Error(`unsupported language: ${lang}`); - } - }, - [selectedCompiler, files] + (lang: WandboxLang) => + async (filenames: string[], files: Readonly>) => { + if (!selectedCompiler) { + return [ + { type: "error" as const, message: "Wandbox is not ready yet." }, + ]; + } + switch (lang) { + case "cpp": + return cppRunFiles(selectedCompiler.cpp, files, filenames); + default: + lang satisfies never; + throw new Error(`unsupported language: ${lang}`); + } + }, + [selectedCompiler] ); return ( diff --git a/app/terminal/worker/jsEval.worker.ts b/app/terminal/worker/jsEval.worker.ts index 1b90c9f..463b5ec 100644 --- a/app/terminal/worker/jsEval.worker.ts +++ b/app/terminal/worker/jsEval.worker.ts @@ -1,11 +1,13 @@ +/// + import type { ReplOutput } from "../repl"; import type { MessageType, WorkerRequest, WorkerResponse } from "./runtime"; let jsOutput: ReplOutput[] = []; // Helper function to capture console output -const originalConsole = globalThis.console; -globalThis.console = { +const originalConsole = self.console; +self.console = { ...originalConsole, // eslint-disable-next-line @typescript-eslint/no-explicit-any log: (...args: any[]) => { @@ -38,7 +40,7 @@ async function runCode({ id, payload }: WorkerRequest["runCode"]) { try { // Execute code directly with eval in the worker global scope // This will preserve variables across calls - const result = globalThis.eval(code); + const result = self.eval(code); if (result !== undefined) { jsOutput.push({ @@ -48,6 +50,7 @@ async function runCode({ id, payload }: WorkerRequest["runCode"]) { } } catch (e) { originalConsole.log(e); + // TODO: stack trace? if (e instanceof Error) { jsOutput.push({ type: "error", @@ -56,7 +59,7 @@ async function runCode({ id, payload }: WorkerRequest["runCode"]) { } else { jsOutput.push({ type: "error", - message: `予期せぬエラー: ${String(e)}`, + message: `${String(e)}`, }); } } @@ -70,13 +73,32 @@ async function runCode({ id, payload }: WorkerRequest["runCode"]) { } satisfies WorkerResponse["runCode"]); } -function runFile({ id }: WorkerRequest["runFile"]) { - const output: ReplOutput[] = [ - { - type: "error", - message: "File execution is not supported in this runtime", - }, - ]; +function runFile({ id, payload }: WorkerRequest["runFile"]) { + const { name, files } = payload; + // pyodide worker などと異なり、複数ファイルを読み込んでimportのようなことをするのには対応していません。 + try { + // Execute code directly with eval in the worker global scope + // This will preserve variables across calls + self.eval(files[name]); + } catch (e) { + originalConsole.log(e); + // TODO: stack trace? + if (e instanceof Error) { + jsOutput.push({ + type: "error", + message: `${e.name}: ${e.message}`, + }); + } else { + jsOutput.push({ + type: "error", + message: `${String(e)}`, + }); + } + } + + const output = [...jsOutput]; + jsOutput = []; // Clear output + self.postMessage({ id, payload: { output, updatedFiles: [] }, @@ -127,7 +149,7 @@ async function restoreState({ id, payload }: WorkerRequest["restoreState"]) { for (const command of commands) { try { - globalThis.eval(command); + self.eval(command); } catch (e) { // If restoration fails, we still continue with other commands originalConsole.error("Failed to restore command:", command, e); diff --git a/app/terminal/worker/pyodide.worker.ts b/app/terminal/worker/pyodide.worker.ts index 0aa5edf..55c4749 100644 --- a/app/terminal/worker/pyodide.worker.ts +++ b/app/terminal/worker/pyodide.worker.ts @@ -1,3 +1,6 @@ +/// +/// + import type { PyodideInterface } from "pyodide"; // import { loadPyodide } from "pyodide"; -> Reading from "node:child_process" is not handled by plugins import { version as pyodideVersion } from "pyodide/package.json"; @@ -32,10 +35,10 @@ function readAllFiles(): Record { async function init({ id, payload }: WorkerRequest["init"]) { const { interruptBuffer } = payload; if (!pyodide) { - (globalThis as WorkerGlobalScope).importScripts(`${PYODIDE_CDN}pyodide.js`); + self.importScripts(`${PYODIDE_CDN}pyodide.js`); // eslint-disable-next-line @typescript-eslint/no-explicit-any - pyodide = await (globalThis as any).loadPyodide({ indexURL: PYODIDE_CDN }); + pyodide = await (self as any).loadPyodide({ indexURL: PYODIDE_CDN }); pyodide.setStdout({ batched: (str: string) => { diff --git a/app/terminal/worker/ruby.worker.ts b/app/terminal/worker/ruby.worker.ts index af39088..9fc396c 100644 --- a/app/terminal/worker/ruby.worker.ts +++ b/app/terminal/worker/ruby.worker.ts @@ -1,3 +1,6 @@ +/// +/// + import { DefaultRubyVM } from "@ruby/wasm-wasi/dist/browser"; import type { RubyVM } from "@ruby/wasm-wasi/dist/vm"; import type { MessageType, WorkerRequest, WorkerResponse } from "./runtime"; @@ -12,12 +15,12 @@ declare global { var stdout: { write: (str: string) => void }; var stderr: { write: (str: string) => void }; } -globalThis.stdout = { +self.stdout = { write(str: string) { stdoutBuffer += str; }, }; -globalThis.stderr = { +self.stderr = { write(str: string) { stderrBuffer += str; }, diff --git a/app/terminal/worker/runtime.tsx b/app/terminal/worker/runtime.tsx index e9129ec..3ecd2a4 100644 --- a/app/terminal/worker/runtime.tsx +++ b/app/terminal/worker/runtime.tsx @@ -70,7 +70,7 @@ export function WorkerProvider({ const workerRef = useRef(null); const [ready, setReady] = useState(false); const mutex = useRef(new Mutex()); - const { files, writeFile } = useEmbedContext(); + const { writeFile } = useEmbedContext(); const messageCallbacks = useRef< // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -204,9 +204,7 @@ export function WorkerProvider({ try { const { output, updatedFiles } = await postMessage("runCode", { code }); - for (const [name, content] of Object.entries(updatedFiles)) { - writeFile(name, content); - } + writeFile(updatedFiles); // Save command to history if interrupt method is 'restart' if (capabilities.current?.interrupt === "restart") { @@ -239,7 +237,10 @@ export function WorkerProvider({ ); const runFiles = useCallback( - async (filenames: string[]): Promise => { + async ( + filenames: string[], + files: Readonly> + ): Promise => { if (filenames.length !== 1) { return [ { @@ -267,13 +268,11 @@ export function WorkerProvider({ name: filenames[0], files, }); - for (const [newName, content] of Object.entries(updatedFiles)) { - writeFile(newName, content); - } + writeFile(updatedFiles); return output; }); }, - [files, ready, writeFile] + [ready, writeFile] ); return ( diff --git a/copyAllDTSFiles.ts b/copyAllDTSFiles.ts new file mode 100644 index 0000000..fe3b647 --- /dev/null +++ b/copyAllDTSFiles.ts @@ -0,0 +1,23 @@ +// node_modules/typescript/lib からd.tsファイルをすべてpublic/typescript/version/にコピーする。 + +import { knownLibFilesForCompilerOptions } from "@typescript/vfs"; +import { compilerOptions } from "./app/terminal/typescript/runtime"; +import ts from "typescript"; +import fs from "node:fs/promises"; +import { existsSync } from "node:fs"; + +const libFiles = knownLibFilesForCompilerOptions(compilerOptions, ts); + +const destDir = `./public/typescript/${ts.version}/`; +await fs.mkdir(destDir, { recursive: true }); + +for (const libFile of libFiles) { + const srcPath = `./node_modules/typescript/lib/${libFile}`; + const destPath = `${destDir}${libFile}`; + if(existsSync(srcPath)) { + await fs.copyFile(srcPath, destPath); + console.log(`Copied ${libFile} to ${destPath}`); + } else { + console.warn(`Source file does not exist: ${srcPath}`); + } +} diff --git a/declatations.d.ts b/declatations.d.ts index 7bd2f6a..2f49eec 100644 --- a/declatations.d.ts +++ b/declatations.d.ts @@ -1 +1,3 @@ declare module "ace-builds/src-min-noconflict/*"; +declare module "prismjs/components/*"; +declare module "mocha/mocha.js"; diff --git a/package-lock.json b/package-lock.json index 258111e..a053403 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@google/genai": "^1.21.0", "@opennextjs/cloudflare": "^1.7.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", @@ -35,6 +36,7 @@ "react-syntax-highlighter": "^16.1.0", "remark-gfm": "^4.0.1", "swr": "^2.3.6", + "typescript": "5.9.3", "zod": "^4.0.17" }, "devDependencies": { @@ -55,7 +57,7 @@ "prettier": "^3.6.2", "prisma": "^6.18.0", "tailwindcss": "^4", - "typescript": "5.9.3", + "tsx": "^4.20.6", "wrangler": "^4.27.0" } }, @@ -12593,6 +12595,18 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript/vfs": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.2.tgz", + "integrity": "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + }, + "peerDependencies": { + "typescript": "*" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -21626,6 +21640,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tsyringe": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", @@ -21753,7 +21787,6 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index c6bae2e..e48da7b 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,15 @@ "name": "my-code", "version": "0.1.0", "private": true, + "type": "module", "scripts": { - "dev": "npm run cf-typegen && next dev", - "build": "npm run cf-typegen && next build", + "dev": "npm run cf-typegen && npm run copyAllDTSFiles && next dev", + "build": "npm run cf-typegen && npm run copyAllDTSFiles && next build", "start": "next start", "lint": "npm run cf-typegen && next lint", "tsc": "npm run cf-typegen && tsc", "format": "prettier --write app/", + "copyAllDTSFiles": "tsx ./copyAllDTSFiles.ts", "cf-preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview --port 3000", "cf-deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy", "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts" @@ -19,6 +21,7 @@ "@google/genai": "^1.21.0", "@opennextjs/cloudflare": "^1.7.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", @@ -41,6 +44,7 @@ "react-syntax-highlighter": "^16.1.0", "remark-gfm": "^4.0.1", "swr": "^2.3.6", + "typescript": "5.9.3", "zod": "^4.0.17" }, "devDependencies": { @@ -61,7 +65,7 @@ "prettier": "^3.6.2", "prisma": "^6.18.0", "tailwindcss": "^4", - "typescript": "5.9.3", + "tsx": "^4.20.6", "wrangler": "^4.27.0" } } diff --git a/public/_headers b/public/_headers index 781edb6..133717c 100644 --- a/public/_headers +++ b/public/_headers @@ -2,3 +2,5 @@ Cache-Control: public,max-age=31536000,immutable Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp +/typescript/* + Cache-Control: public,max-age=31536000,immutable diff --git a/tsconfig.json b/tsconfig.json index 91a8094..fc693c6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext", "webworker"], + "lib": ["dom", "dom.iterable", "es2023"], "allowJs": true, "skipLibCheck": true, "strict": true,