webcontainer-sandbox-react
Version:
A React component library for WebContainer-based code sandboxes
1,541 lines (1,520 loc) • 48 kB
JavaScript
import { jsx as l, jsxs as k, Fragment as me } from "react/jsx-runtime";
import { createContext as ke, useContext as Ee, useState as v, useEffect as _, useRef as L, useCallback as M, useMemo as He } from "react";
import { WebContainer as Qe } from "@webcontainer/api";
import { CodeOutlined as Xe, WarningOutlined as Ge, ToolOutlined as Je, CloseOutlined as Ye, DesktopOutlined as Ze, MobileOutlined as et, LoadingOutlined as tt, FolderOpenOutlined as nt, FolderOutlined as rt, FileTextOutlined as ot, FileAddOutlined as be, FolderAddOutlined as ve, EditOutlined as st, CopyOutlined as it, ScissorOutlined as lt, DeleteOutlined as at } from "@ant-design/icons";
import { Button as K, Space as Y, Card as ct, Collapse as dt, Alert as ut, Typography as ie, Spin as ft, message as B, Tooltip as xe, Dropdown as pt, Modal as he, Input as Ce } from "antd";
import { Terminal as ht } from "xterm";
import "xterm/css/xterm.css";
import mt from "@monaco-editor/react";
const Fe = ke({
webContainer: null,
iframeSrc: ""
}), ae = () => Ee(Fe), Nt = ({ children: i }) => {
const [t, e] = v(null), [n, o] = v("");
return _(() => {
(async () => {
if (t)
return;
const s = await Qe.boot({ workdirName: "project" });
s.on("error", (f) => {
console.error("WebContainer error:", f);
}), s.on("port", (f, u, p) => {
u === "close" ? (o(""), console.log("port closed")) : u === "open" && (console.log(`Server running on ${p}`), o(p));
}), e(s);
})();
}, []), /* @__PURE__ */ l(Fe.Provider, { value: { webContainer: t, iframeSrc: n }, children: i });
}, Se = ke({
loading: !1,
loadingContent: "",
startProcess: null,
writer: null,
output: null,
processOutput: null,
sessionErrors: [],
clearErrors: null,
files: {},
setFile: null,
spawn: null
}), Z = () => Ee(Se), gt = () => `task_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
function wt(i = {}) {
const { cancelPending: t = !0, cleanupBeforeNew: e = !0, debug: n = !1 } = i, o = L([]), a = L(null), s = L(null), f = L(!1), u = L(!1), p = M(
(...w) => {
n && console.log("[AsyncQueue]", ...w);
},
[n]
), h = M(async () => {
if (!f.current) {
for (f.current = !0; o.current.length > 0 && !u.current; ) {
let w = null;
if (t) {
for (let y = 0; y < o.current.length - 1; y++) {
const b = o.current[y];
b !== a.current && (b.cancelled = !0, p(`取消等待任务: ${b.id}`));
}
for (let y = o.current.length - 1; y >= 0; y--) {
const b = o.current[y];
if (!b.cancelled) {
w = b;
break;
}
}
} else
w = o.current.find((y) => !y.cancelled) || null;
if (o.current = o.current.filter((y) => !y.cancelled), !w)
break;
if (e && a.current && a.current !== w) {
if (p(`检测到新任务,开始清理当前任务: ${a.current.id}`), a.current.cleanup)
try {
await a.current.cleanup(), p(`清理完成: ${a.current.id}`);
} catch (y) {
p(`清理失败: ${a.current.id}`, y);
}
a.current = null, s.current = null;
} else s.current && s.current.cleanup && (await s.current.cleanup(), p(`清理完成: ${s.current.id}`), s.current = null);
if (!w.cancelled) {
a.current = w;
try {
p(`开始执行任务: ${w.id}`), await w.execute(), w.cancelled ? p(`任务被取消: ${w.id}`) : (p(`任务执行完成: ${w.id}`), w.cleanup && (s.current = w));
} catch (y) {
const b = y;
p(`任务执行失败: ${w.id}`, b);
}
o.current = o.current.filter((y) => y !== w);
}
}
a.current = null, f.current = !1, p("队列处理完成");
}
}, [t, e, p]), g = M(
async (w, y) => new Promise((b, E) => {
const N = gt(), P = {
id: N,
cancelled: !1,
timestamp: Date.now(),
execute: async () => {
try {
const S = await w();
return P.cancelled || b(S), S;
} catch (S) {
throw P.cancelled || E(S), S;
}
},
cleanup: y
};
o.current.push(P), p(`任务入队: ${N}, 队列长度: ${o.current.length}`), h();
}),
[h, p]
);
_(() => () => {
g(() => new Promise((w) => {
setTimeout(w, 1e3);
}));
}, []);
const T = M(() => {
o.current.forEach((w) => {
w.cancelled = !0;
}), o.current = [], p("所有任务已取消");
}, [p]);
return {
enqueue: g,
cancelAll: T
};
}
const yt = {
cleanupBeforeNew: !0,
debug: !0,
cancelPending: !0
};
function bt(i, t) {
const { enqueue: e, cancelAll: n } = wt(yt), o = M(async () => {
try {
return await e(i, t);
} catch (s) {
throw console.log("Error:", s), s;
}
}, [e, i, t]), a = M(() => {
n();
}, [n]);
return {
execute: o,
reset: a
};
}
const vt = `
(function() {
"use strict";
// 数据清理函数 - 移除不可序列化的对象
function sanitizeData(obj, depth = 0) {
if (depth > 10) return "[深度限制]";
if (obj === null || obj === undefined) {
return obj;
}
if (typeof obj === "string" || typeof obj === "number" || typeof obj === "boolean") {
return obj;
}
if (obj instanceof Error) {
return {
name: obj.name,
message: obj.message,
stack: obj.stack
};
}
if (obj instanceof Request || obj instanceof Response || obj instanceof ReadableStream) {
return "[不可序列化对象: " + obj.constructor.name + "]";
}
if (obj instanceof Event) {
return {
type: obj.type,
target: obj.target ? obj.target.tagName || obj.target.constructor.name : null,
timeStamp: obj.timeStamp
};
}
if (Array.isArray(obj)) {
return obj.map(item => sanitizeData(item, depth + 1));
}
if (typeof obj === "object") {
const result = {};
for (const key in obj) {
try {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (typeof value === "function") {
result[key] = "[函数]";
} else if (value && typeof value === "object") {
// 检查是否是不可序列化的对象
if (value instanceof Request || value instanceof Response ||
value instanceof ReadableStream || value instanceof AbortController ||
value instanceof FormData || value instanceof File) {
result[key] = "[不可序列化对象: " + value.constructor.name + "]";
} else {
result[key] = sanitizeData(value, depth + 1);
}
} else {
result[key] = value;
}
}
} catch (e) {
result[key] = "[访问错误: " + e.message + "]";
}
}
return result;
}
return String(obj);
}
function reportError(errorData) {
console.log("准备报告错误:", errorData);
try {
// 清理错误数据
const cleanErrorData = sanitizeData(errorData);
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: "webcontainer-error",
error: cleanErrorData,
source: "webcontainer",
timestamp: Date.now()
}, "*");
console.log("错误已发送到父页面");
}else if (window.top && window.top !== window) {
window.top.postMessage({
type: "webcontainer-error",
error: cleanErrorData,
source: "webcontainer",
timestamp: Date.now()
}, "*");
}
} catch (e) {
console.warn("无法发送错误报告:", e);
// 尝试发送简化版本的错误信息
try {
const simplifiedError = {
type: errorData.type || "unknown-error",
message: String(errorData.message || "未知错误"),
timestamp: Date.now()
};
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: "webcontainer-error",
error: simplifiedError,
source: "webcontainer"
}, "*");
}
} catch (fallbackError) {
console.error("连简化错误都无法发送:", fallbackError);
}
}
}
// 全局错误捕获
window.addEventListener("error", function(event) {
reportError({
type: "javascript-error",
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error ? event.error.stack : null,
timestamp: Date.now()
});
}, true);
// Promise rejection
window.addEventListener("unhandledrejection", function(event) {
console.log("捕获到Promise rejection:", event);
reportError({
type: "unhandled-rejection",
message: event.reason ? String(event.reason) : "Unknown rejection",
stack: event.reason && event.reason.stack ? event.reason.stack : null,
reason: event.reason,
timestamp: Date.now()
});
});
// // React错误补充
// const originalConsoleError = console.error;
// console.error = function(...args) {
// const errorMessage = args.map(arg => {
// if (typeof arg === "object") {
// try {
// return JSON.stringify(arg, null, 2);
// } catch {
// return String(arg);
// }
// }
// return String(arg);
// }).join(" ");
// if (errorMessage.includes("React") ||
// errorMessage.includes("component") ||
// errorMessage.includes("render")) {
// reportError({
// type: "react-error",
// message: errorMessage,
// timestamp: Date.now()
// });
// }
// originalConsoleError.apply(console, args);
// };
// // 网络错误监听
// const originalFetch = window.fetch;
// window.fetch = function(...args) {
// return originalFetch.apply(this, args).catch(error => {
// reportError({
// type: "network-error",
// message: "Fetch failed: " + error.message,
// url: args[0],
// timestamp: Date.now()
// });
// throw error;
// });
// };
// // 资源加载错误
// window.addEventListener("error", function(event) {
// if (event.target !== window) {
// reportError({
// type: "resource-error",
// message: "Failed to load " + event.target.tagName + ": " + (event.target.src || event.target.href),
// element: event.target.tagName,
// source: event.target.src || event.target.href,
// timestamp: Date.now()
// });
// }
// }, true);
})();
`, xt = (i) => i.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
class Ct extends EventTarget {
constructor(t, e) {
super(), this.buffer = [], this.isReading = !1, this.maxBufferSize = 1e3, this.sourceStream = t, this.abortController = new AbortController(), this.startReading(), this.onErrorStream = e;
}
async startReading() {
if (this.isReading) return;
this.isReading = !0;
const t = this.sourceStream.getReader();
try {
for (; !this.abortController.signal.aborted; ) {
const { done: e, value: n } = await t.read();
if (e) break;
(n.startsWith("Error:") || n.indexOf("[22m \x1B[31m\x1B[1m[vite]") > -1) && this.onErrorStream(xt(n)), this.buffer.push(n), this.buffer.length > this.maxBufferSize && this.buffer.shift(), this.dispatchEvent(new CustomEvent("data", { detail: n }));
}
} catch (e) {
this.abortController.signal.aborted || this.dispatchEvent(new CustomEvent("error", { detail: e }));
} finally {
t.releaseLock(), this.abortController.signal.aborted || this.dispatchEvent(new CustomEvent("end")), this.isReading = !1;
}
}
getBuffer() {
return [...this.buffer];
}
createStream() {
const t = this.getBuffer();
let e = !0;
return new ReadableStream({
start: (n) => {
if (this.abortController.signal.aborted) {
n.close();
return;
}
t.forEach((u) => {
e && !this.abortController.signal.aborted && n.enqueue(u);
});
const o = (u) => {
const p = u;
if (e && !this.abortController.signal.aborted)
try {
n.enqueue(p.detail);
} catch (h) {
console.error("Failed to enqueue data:", h);
}
}, a = () => {
if (e)
try {
n.close();
} catch (u) {
console.error("Failed to close controller:", u);
}
}, s = (u) => {
const p = u;
if (e)
try {
n.error(p.detail);
} catch (h) {
console.error("Failed to error controller:", h);
}
}, f = () => {
e = !1;
try {
n.close();
} catch (u) {
console.error("Failed to close controller on abort:", u);
}
};
return this.addEventListener("data", o, { signal: this.abortController.signal }), this.addEventListener("end", a, { signal: this.abortController.signal }), this.addEventListener("error", s, { signal: this.abortController.signal }), this.abortController.signal.addEventListener("abort", f), () => {
e = !1;
};
},
cancel() {
e = !1;
}
});
}
cleanup() {
this.buffer = [], this.abortController.abort();
}
// 提供类型安全的方法,但不重写 addEventListener
onData(t, e) {
this.addEventListener("data", t, e);
}
onEnd(t, e) {
this.addEventListener("end", t, e);
}
onError(t, e) {
this.addEventListener("error", t, e);
}
offData(t, e) {
this.removeEventListener("data", t, e);
}
offEnd(t, e) {
this.removeEventListener("end", t, e);
}
offError(t, e) {
this.removeEventListener("error", t, e);
}
}
const Mt = (i) => {
const t = {};
return i.forEach(({ name: e, content: n }) => {
const o = e.split("/");
let a = t;
for (let s = 0; s < o.length; s++) {
const f = o[s];
s === o.length - 1 ? a[f] = {
file: {
contents: n
}
} : (a[f] || (a[f] = {
directory: {}
}), a = a[f].directory);
}
}), t;
}, zt = ({
sessionKey: i,
initFiles: t,
buildCommand: e = "yarn",
startCommand: n = "yarn dev",
children: o,
fileObservers: a,
onError: s
}) => {
const [f, u] = v(""), [p, h] = v(!0), { webContainer: g, iframeSrc: T } = ae(), w = L(null), [y, b] = v(null), [E, N] = v(null), [P, S] = v(null), [U, ne] = v(null), [ce, H] = v([]), X = L([]), ee = M(() => {
H([]);
}, []);
_(() => {
g && (ee(), g.mount(Mt(t)));
}, [g, t]), _(() => {
p && T && h(!1);
}, [T, p]);
const Q = M(
(x) => {
H((j) => j.find((R) => R.message === x.message && R.from === x.from && R.type === x.type) ? j : [...j, x]), s && s(x);
},
[s]
), re = M(async () => {
if (!g || !t || !t.length || !i)
return;
u("(1/4)初始化容器...");
const x = {
...a,
"package.json": async () => {
e && await (await g.spawn(e)).exit;
}
}, j = {};
for (const F of t) {
const { name: $, content: W } = F;
if (u(`(2/4)写入文件${$}...`), console.log(`Writing file ${$}...`), $.indexOf("/") > -1) {
const V = $.substring(0, $.lastIndexOf("/"));
await g.fs.mkdir(V, { recursive: !0 });
}
if (x && x[$] && (j[$] = W), $ === "index.html") {
let V = W;
W.indexOf("</body>") > -1 ? V = W.replace("</body>", '<script src="./error-monitor.js"><\/script></body>') : V = W + '<script src="./error-monitor.js"><\/script>', await g.fs.writeFile($, V);
} else
await g.fs.writeFile($, W);
}
if (await g.fs.writeFile("error-monitor.js", vt), console.log("Writing files done"), u("(3/4)构建中..."), e && await (await g.spawn(e)).exit, x)
for (const F in x) {
X.current.push(
g.fs.watch(F, async () => {
const W = await g.fs.readFile(F, "utf-8");
j[F] !== W && (x[F](W, j[F]), j[F] = W);
})
);
const $ = await g.fs.readFile(F, "utf-8");
j[F] !== $ && (x[F]($, j[F]), j[F] = $);
}
console.log("Build done");
const R = await g.spawn("/bin/jsh", ["--osc"], {
terminal: {
cols: 60,
rows: 24
}
});
w.current = R, b(R);
const A = R.input.getWriter();
N(A);
const { output: z } = R;
S(z), ne(new Ct(z, (F) => Q({ type: "terminal", message: F }))), u("(4/4)启动中..."), n && await A.write(`${n}
`), console.log("Start done");
}, [i, g]), de = M(async () => {
g && (X.current.forEach((x) => {
x.close();
}), X.current = [], w.current && (console.log("kill process"), await w.current.kill(), w.current = null), await Promise.all([
...t.map((x) => x.name).map((x) => g.fs.rm(x)),
g.fs.rm("yarn.lock")
]), console.log("cleanup done"), ee());
}, [g]), { execute: G } = bt(re, de);
_(() => {
g && (h(!0), G());
}, [g, G]);
const ue = He(() => t.reduce((x, j) => (x[j.name] = j.content, x), {}), [t]), fe = M(
async (x, j) => {
if (g) {
if (console.log(`Writing file ${x}...`), x.indexOf("/") > -1) {
const R = x.substring(0, x.lastIndexOf("/"));
await g.fs.mkdir(R, { recursive: !0 });
}
await g.fs.writeFile(x, j);
}
},
[g]
), te = M(
async (x) => {
g && await (await g.spawn(x)).exit;
},
[g, E]
);
return /* @__PURE__ */ l(
Se.Provider,
{
value: {
loading: p,
loadingContent: f,
startProcess: y,
writer: E,
output: P,
processOutput: U,
onError: Q,
sessionErrors: ce,
clearErrors: ee,
setFile: fe,
spawn: te,
files: ue
},
children: o
}
);
}, Bt = ({ router: i = "", style: t, className: e, width: n = "100%", height: o = "100%" }) => {
const { loading: a, onError: s } = Z(), { iframeSrc: f } = ae(), u = L(null);
return _(() => {
if (!u.current) return;
const p = ({ data: { type: h }, data: g }) => {
if (h === "webcontainer-error") {
console.error("WebContainer error:", g);
const {
error: { message: T, filename: w, lineno: y, colno: b }
} = g;
if (s && T) {
const E = w ? `${w.indexOf("webcontainer-api.io/") > -1 ? w.substring(w.indexOf("webcontainer-api.io/") + 20) : w}:${y}:${b}` : "";
s({
type: "browser",
message: T,
from: E
});
}
}
};
return window.addEventListener("message", p), () => {
window.removeEventListener("message", p);
};
}, [f, s, a]), a || !f ? null : /* @__PURE__ */ l(
"iframe",
{
ref: u,
src: i ? `${f}/${i}` : f,
className: e,
style: { border: "none", width: n, height: o, ...t }
}
);
}, Me = ({ active: i, onTerminalReady: t }) => {
const e = L(null);
return _(() => {
if (!e.current || !t) return;
const n = new ht({
cursorBlink: !0,
rows: 24,
cols: 60,
theme: {
background: "#1e1e1e",
foreground: "#f0f0f0"
},
fontSize: 14,
fontFamily: 'Menlo, Monaco, "Courier New", monospace'
});
n.open(e.current), t(n);
}, [t]), /* @__PURE__ */ l(
"div",
{
ref: e,
style: {
display: i ? "block" : "none"
}
}
);
}, _t = () => {
const [i, t] = v(!1), [e, n] = v(0), [o, a] = v(0), { startProcess: s, writer: f, processOutput: u } = Z(), { webContainer: p } = ae(), h = L(null), g = M((y, b) => {
const E = y.input.getWriter(), { output: N } = y;
N.pipeTo(
new WritableStream({
write(P) {
b.write(P);
}
})
), b.onData((P) => {
E.write(P);
});
}, []), T = M(
async (y) => {
if (!p)
return;
const b = await p.spawn("/bin/jsh", ["--osc"], {
terminal: {
cols: 60,
rows: 24
}
});
g(b, y);
},
[p, g]
), w = M(
async (y) => {
if (!s || !f || !u)
return;
const b = u.createStream();
h.current = b, b.pipeTo(
new WritableStream({
write(E) {
y.write(E);
}
})
), y.onData((E) => {
f.write(E);
});
},
[s, f]
);
return /* @__PURE__ */ k(me, { children: [
/* @__PURE__ */ l(
K,
{
type: "primary",
shape: "circle",
size: "large",
icon: /* @__PURE__ */ l(Xe, {}),
onClick: () => t(!i),
style: {
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)"
}
}
),
/* @__PURE__ */ k(
"div",
{
style: {
position: "fixed",
bottom: "80px",
right: "20px",
width: "600px",
height: "400px",
backgroundColor: "#1e1e1e",
color: "#f0f0f0",
borderRadius: "5px",
zIndex: 1e3,
display: i ? "flex" : "none",
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
overflow: "hidden"
},
children: [
/* @__PURE__ */ k(
Y,
{
direction: "vertical",
style: {
padding: "8px 12px",
backgroundColor: "#333",
display: "flex",
justifyContent: "flex-start",
alignItems: "center"
},
children: [
Array.from({ length: e + 1 }, (y, b) => /* @__PURE__ */ l(
K,
{
type: b === o ? "primary" : "text",
size: "small",
onClick: () => a(b),
style: { color: "#f0f0f0" },
children: b === 0 ? "Main" : `Terminal ${b + 1}`
},
b
)),
/* @__PURE__ */ l(
K,
{
type: "text",
size: "small",
onClick: () => n((y) => y + 1),
style: { color: "#f0f0f0" },
children: "+"
}
)
]
}
),
s && /* @__PURE__ */ l(Me, { active: o === 0, onTerminalReady: w }),
Array.from({ length: e }, (y, b) => {
const E = b + 1 === o;
return /* @__PURE__ */ l(Me, { active: E, onTerminalReady: T }, b);
})
]
}
)
] });
}, Vt = ({ onFix: i, hanging: t = !1 }) => {
const [e, n] = v([]), [o, a] = v(!1), [s, f] = v(!0), { sessionErrors: u, clearErrors: p } = Z(), h = M(async () => {
a(!0), await i(u), a(!1), p && p();
}, [u, p]);
return !u || u.length === 0 ? null : /* @__PURE__ */ k(me, { children: [
/* @__PURE__ */ l(
K,
{
type: "primary",
shape: "circle",
size: "large",
icon: /* @__PURE__ */ l(Ge, {}),
onClick: () => f(!s),
style: {
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)"
}
}
),
/* @__PURE__ */ k(
ct,
{
title: /* @__PURE__ */ k(Y, { children: [
/* @__PURE__ */ l(ie.Text, { type: "danger", children: "潜在问题已检测到" }),
/* @__PURE__ */ k(ie.Text, { type: "secondary", children: [
u.length,
" 个错误"
] })
] }),
extra: /* @__PURE__ */ l(K, { type: "text", icon: /* @__PURE__ */ l(Ye, {}), onClick: () => f(!1) }),
style: {
width: 500,
position: "fixed",
right: 20,
bottom: 20,
zIndex: 1e3,
boxShadow: "0 3px 6px rgba(0,0,0,0.16)",
display: s ? "block" : "none"
},
children: [
/* @__PURE__ */ l(
dt,
{
activeKey: e,
onChange: n,
items: u.map((g, T) => ({
key: T,
label: /* @__PURE__ */ k(Y, { children: [
/* @__PURE__ */ l(ie.Text, { type: "danger", children: g.type === "terminal" ? "终端错误" : "浏览器错误" }),
g.from && /* @__PURE__ */ l(ie.Text, { ellipsis: !0, style: { maxWidth: 300 }, children: g.from })
] }),
children: /* @__PURE__ */ l("div", { children: /* @__PURE__ */ l(ut, { type: "error", message: g.message }) })
}))
}
),
/* @__PURE__ */ l("div", { style: { marginTop: 16, textAlign: "right" }, children: /* @__PURE__ */ k(Y, { children: [
/* @__PURE__ */ l(K, { disabled: t, onClick: h, loading: o, type: "primary", icon: /* @__PURE__ */ l(Je, {}), children: "尝试修复" }),
/* @__PURE__ */ l(K, { onClick: () => f(!1), children: "忽略" })
] }) })
]
}
)
] });
}, qt = ({ routers: i, style: t, className: e, width: n = 1440, height: o = 1024, scale: a = 0.5 }) => {
const { loading: s } = Z(), { iframeSrc: f } = ae(), u = {
width: n * a,
height: o * a
}, p = {
width: n,
height: o
};
return s || !f ? null : /* @__PURE__ */ l("div", { className: e, style: { position: "relative", ...t }, children: /* @__PURE__ */ l(Y, { wrap: !0, size: "large", children: i.map((h) => /* @__PURE__ */ k("div", { style: { border: "1px solid #ccc", padding: "10px", borderRadius: "5px", margin: "10px" }, children: [
h,
/* @__PURE__ */ l("div", { style: { ...u }, children: /* @__PURE__ */ l(
"iframe",
{
src: `${f}/${h}`,
style: { ...p, transform: `scale(${a})`, transformOrigin: "0 0" }
}
) })
] }, h)) }) });
}, Kt = ({ value: i, onChange: t }) => /* @__PURE__ */ l(
K,
{
type: "primary",
shape: "circle",
size: "large",
icon: i === "mobile" ? /* @__PURE__ */ l(Ze, {}) : /* @__PURE__ */ l(et, {}),
onClick: () => t && t(i === "mobile" ? "pc" : "mobile"),
style: {
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)"
}
}
), kt = ({ style: i, className: t, children: e }) => {
const { loading: n, loadingContent: o } = Z();
return /* @__PURE__ */ k("div", { className: t, style: { position: "relative", width: "100%", height: "100%", ...i }, children: [
n && /* @__PURE__ */ k(me, { children: [
/* @__PURE__ */ l("div", { style: { display: "flex", justifyContent: "center", alignItems: "center", height: "100%" }, children: /* @__PURE__ */ l(ft, { size: "large" }) }),
/* @__PURE__ */ k(
"div",
{
style: {
position: "absolute",
left: "10px",
bottom: "10px",
display: "flex",
alignItems: "center",
color: "#999",
zIndex: 1e3
},
children: [
/* @__PURE__ */ l(
tt,
{
style: {
color: "#999",
fontSize: "12px"
}
}
),
o
]
}
)
] }),
e
] });
}, Et = ({ style: i, className: t, children: e }) => /* @__PURE__ */ l(
"div",
{
className: t,
style: {
position: "fixed",
bottom: "20px",
right: "20px",
zIndex: 1e3,
display: "flex",
justifyItems: "flex-end",
gap: "10px",
...i
},
children: e
}
), Ut = { Layout: kt, Footer: Et }, Ft = ({
fileSystem: i,
onFileClick: t,
onFolderToggle: e,
onContextMenu: n,
activeFileId: o,
openFiles: a
}) => {
const [s, f] = v(null), u = (h, g = 0) => {
var N, P;
if (h.id === "root")
return /* @__PURE__ */ l("div", { children: Array.from(((N = h.children) == null ? void 0 : N.values()) || []).map((S) => u(S, g)) }, h.id);
const T = s === h.id, w = o === h.id, y = h.type === "file" && ((P = a.get(h.id)) == null ? void 0 : P.modified), b = (S) => {
S.stopPropagation(), f(h.id), h.type === "file" ? t(h.id) : e(h.id);
}, E = (S) => {
S.stopPropagation(), f(h.id), n(S, h.id);
};
return /* @__PURE__ */ k("div", { children: [
/* @__PURE__ */ k(
"div",
{
className: `file-tree-item ${h.type} ${T ? "selected" : ""} ${w ? "active" : ""} ${y ? "modified" : ""}`,
style: { paddingLeft: `${g * 16 + 8}px` },
onClick: b,
onContextMenu: E,
children: [
/* @__PURE__ */ l("span", { className: "icon", children: h.type === "folder" ? h.expanded ? /* @__PURE__ */ l(nt, {}) : /* @__PURE__ */ l(rt, {}) : /* @__PURE__ */ l(ot, {}) }),
/* @__PURE__ */ l("span", { className: "label", children: h.name }),
y && /* @__PURE__ */ l("span", { className: "modified-indicator", children: "●" })
]
}
),
h.type === "folder" && h.expanded && h.children && /* @__PURE__ */ l("div", { children: Array.from(h.children.values()).map((S) => u(S, g + 1)) })
] }, h.id);
}, p = i.get("root");
return p ? /* @__PURE__ */ l("div", { className: "file-tree", children: u(p) }) : null;
}, St = ({ openFiles: i, fileSystem: t, activeFileId: e, onTabClick: n, onTabClose: o }) => /* @__PURE__ */ l("div", { className: "tab-bar", children: Array.from(i.entries()).map(([a, s]) => {
const f = t.get(a);
if (!f) return null;
const u = e === a, p = s.modified;
return /* @__PURE__ */ k(
"div",
{
className: `tab ${u ? "active" : ""} ${p ? "modified" : ""}`,
onClick: () => n(a),
children: [
/* @__PURE__ */ l("span", { className: "tab-label", children: f.name }),
/* @__PURE__ */ l(
"button",
{
className: "tab-close",
onClick: (h) => {
h.stopPropagation(), o(a);
},
children: "×"
}
)
]
},
a
);
}) }), je = (i) => {
var n;
const t = ((n = i.split(".").pop()) == null ? void 0 : n.toLowerCase()) || "";
return {
js: "javascript",
ts: "typescript",
jsx: "javascript",
tsx: "typescript",
html: "html",
css: "css",
scss: "scss",
json: "json",
md: "markdown",
py: "python",
java: "java",
c: "c",
cpp: "cpp",
cs: "csharp",
php: "php",
go: "go",
rs: "rust",
sql: "sql",
yaml: "yaml",
yml: "yaml"
}[t] || "plaintext";
}, jt = (i, t, e) => {
const n = i.get(t);
if (!(n != null && n.children)) return null;
for (const [o, a] of n.children)
if (a.name === e) return a;
return null;
}, J = (i, t, e, n = "") => {
const o = i.get(t);
if (!o || o.type !== "folder") return null;
const a = `${o.fullPath}${e}`, s = a, f = {
id: s,
name: e,
fullPath: a,
type: "file",
content: n,
parent: t,
modified: !1,
language: je(e)
};
return i.set(s, f), o.children.set(s, f), f;
}, le = (i, t, e) => {
const n = i.get(t);
if (!n || n.type !== "folder") return null;
const o = `${n.fullPath}${e}/`, a = o, s = {
id: a,
name: e,
fullPath: o,
type: "folder",
children: /* @__PURE__ */ new Map(),
parent: t,
expanded: !1
};
return i.set(a, s), n.children.set(a, s), s;
}, Ot = (i, t, e) => {
const n = t.split("/").filter(Boolean);
let o = "root";
for (let s = 0; s < n.length - 1; s++) {
const f = n[s], u = jt(i, o, f);
if (u)
o = u.id;
else {
const p = le(i, o, f);
p && (o = p.id);
}
}
const a = n[n.length - 1];
J(i, o, a, e);
}, Oe = (i) => {
const t = {
id: i.id,
name: i.name,
fullPath: i.fullPath,
type: i.type,
content: i.content,
children: /* @__PURE__ */ new Map()
};
if (i.type === "folder" && i.children)
for (const [e, n] of i.children) {
const o = Oe(n);
t.children.set(o.id, o);
}
return t;
}, Pe = (i, t, e) => {
const n = i.get(e);
if (!(n != null && n.children)) return !1;
for (const [o, a] of n.children)
if (a.name === t) return !0;
return !1;
}, Pt = (i, t, e) => {
const n = i.get(e);
if (!(n != null && n.children)) return t;
let o = 1, a = t;
for (; Pe(i, a, e); ) {
const s = t.includes(".") ? t.substring(t.lastIndexOf(".")) : "";
a = `${t.includes(".") ? t.substring(0, t.lastIndexOf(".")) : t}_copy${o > 1 ? o : ""}${s}`, o++;
}
return a;
}, $e = (i, t, e) => {
const n = Pt(i, t.name, e);
if (t.type === "file")
J(i, e, n, t.content || "");
else if (t.children) {
const o = le(i, e, n);
if (o)
for (const [a, s] of t.children)
$e(i, s, o.id);
}
}, Te = (i, t) => {
const e = i.get(t);
if (e) {
if (e.type === "folder" && e.children)
for (const [n] of e.children)
Te(i, n);
i.delete(t);
}
}, Ht = () => {
const { files: i, setFile: t } = Z(), [e, n] = v(/* @__PURE__ */ new Map()), [o, a] = v(/* @__PURE__ */ new Map()), [s, f] = v(null), [u, p] = v({
x: 0,
y: 0,
targetId: "",
show: !1
}), [h, g] = v(null), [T, w] = v(300), [y, b] = v(!1), [E, N] = v("file"), [P, S] = v("root"), [U, ne] = v(""), [ce, H] = v(!1), [X, ee] = v(""), [Q, re] = v(""), [de, G] = v(!1), [ue, fe] = v(""), [te, x] = v("file"), [j, R] = v(""), A = L(null), z = L(null), F = L(!1);
_(() => {
const r = {
id: "root",
name: "root",
fullPath: "/",
type: "folder",
children: /* @__PURE__ */ new Map(),
expanded: !0
}, d = /* @__PURE__ */ new Map();
if (d.set("root", r), Object.entries(i).forEach(([c, m]) => {
Ot(d, c, m);
}), Object.keys(i).length === 0) {
J(d, "root", "index.js", 'console.log("Hello World!");');
const c = le(d, "root", "src");
c && (J(
d,
c.id,
"app.js",
`export default function App() {
return "Hello from App";
}`
), J(
d,
c.id,
"utils.js",
`export const utils = {
formatDate: (date) => {
return date.toLocaleDateString();
}
};`
));
}
n(d);
}, []);
const $ = M(
(r) => {
n((d) => {
const c = new Map(d), m = c.get(r);
if (!m || r === "root") return d;
if (m.type === "folder" && m.children)
for (const [O] of m.children)
Te(c, O);
m.type === "file" && (a((O) => {
var q;
const D = new Map(O), I = D.get(r);
return I && ((q = I.model) == null || q.dispose(), D.delete(r)), D;
}), s === r && f(null));
const C = c.get(m.parent);
return C != null && C.children && C.children.delete(r), c.delete(r), c;
});
},
[s]
), W = M(
(r, d) => {
if (!d.trim() || r === "root") return !1;
let c = !0;
return n((m) => {
const C = new Map(m), O = C.get(r);
if (!O) return m;
const D = C.get(O.parent);
if (D != null && D.children) {
for (const [I, q] of D.children)
if (q.id !== r && q.name === d)
return B.error("该名称已存在"), c = !1, m;
}
if (O.name = d, O.type === "file") {
O.language = je(d);
const I = o.get(r);
I && z.current && z.current.editor.setModelLanguage(I.model, O.language);
}
return C;
}), c;
},
[o]
), V = M(
(r) => {
const d = e.get(r);
d && r !== "root" && g(Oe(d));
},
[e]
), ge = M(
(r) => {
h && n((d) => {
const c = new Map(d), m = c.get(r);
return !m || m.type !== "folder" ? d : ($e(c, h, r), c);
});
},
[h]
), we = M(
(r) => {
const d = e.get(r);
if (!(!d || d.type !== "file")) {
if (o.has(r)) {
f(r);
return;
}
a((c) => {
const m = new Map(c), C = {
id: r,
model: null,
modified: !1,
viewState: null
};
return m.set(r, C), m;
}), f(r);
}
},
[e, o]
), De = M(
(r) => {
const d = o.get(r);
if (d) {
if (d.modified) {
const c = e.get(r);
if (!window.confirm(`"${c == null ? void 0 : c.name}" 有未保存的更改,确定要关闭吗?`))
return;
}
if (a((c) => {
const m = new Map(c), C = m.get(r);
return C != null && C.model && C.model.dispose(), m.delete(r), m;
}), s === r) {
const c = Array.from(o.keys()).filter((m) => m !== r);
f(c.length > 0 ? c[c.length - 1] : null);
}
}
},
[o, e, s]
), Ie = M(
(r, d) => {
n((c) => {
var O;
const m = new Map(c), C = m.get(r);
return C && (C.content = d), t && t(((O = m.get(r)) == null ? void 0 : O.fullPath) || "", d), m;
}), a((c) => {
const m = new Map(c), C = m.get(r);
return C && (C.modified = !1), m;
});
},
[t]
), oe = M(
(r) => {
if (!A.current || !z.current) return;
const d = e.get(r);
if (!d || d.type !== "file") return;
let c = o.get(r);
if (!c)
return;
if (!(c != null && c.model)) {
const C = z.current.editor.createModel(
d.content || "",
d.language,
z.current.Uri.file(d.fullPath)
);
C._fileId = r, a((O) => {
const D = new Map(O), I = D.get(r);
return I ? (I.model = C, D.set(r, I), D) : O;
}), c = { ...c, model: C };
}
if (A.current.getModel() && s && s !== r) {
const C = o.get(s);
C && (C.viewState = A.current.saveViewState());
}
A.current.setModel(c.model), c.viewState && A.current.restoreViewState(c.viewState), A.current.focus();
},
[e, o, s]
), Le = (r, d) => {
A.current = r, z.current = d, r.addCommand(d.KeyMod.CtrlCmd | d.KeyCode.KeyS, () => {
const c = r.getValue(), m = r.getModel()._fileId;
Ie(m, c);
}), r.onDidChangeModelContent(() => {
var m;
const c = r.getModel()._fileId;
if (c) {
const C = r.getValue(), O = ((m = e.get(c)) == null ? void 0 : m.content) || "";
a((D) => {
const I = new Map(D), q = I.get(c);
return q && (q.modified = C !== O), I;
});
}
}), s && oe(s);
};
_(() => {
s ? oe(s) : A.current && (A.current = null);
}, [s, oe]);
const Re = M((r) => {
n((d) => {
const c = new Map(d), m = c.get(r);
return m && m.type === "folder" && (m.expanded = !m.expanded), c;
});
}, []), se = (r, d) => {
N(r), S(d), ne(""), b(!0);
}, Ae = () => {
if (!U.trim()) {
B.error("名称不能为空");
return;
}
if (Pe(e, U, P)) {
B.error("该名称已存在");
return;
}
n((r) => {
const d = new Map(r);
if (E === "file") {
const m = J(d, P, U);
m && setTimeout(() => we(m.id), 0);
} else
le(d, P, U);
const c = d.get(P);
return c && c.type === "folder" && (c.expanded = !0), d;
}), b(!1), B.success(`${E === "file" ? "文件" : "文件夹"}创建成功`);
}, We = (r) => {
const d = e.get(r);
d && (ee(r), re(d.name), H(!0));
}, Ne = () => {
if (!Q.trim()) {
B.error("名称不能为空");
return;
}
const r = e.get(X);
if (!r) return;
if (Q === r.name) {
H(!1);
return;
}
W(X, Q) && (H(!1), B.success("重命名成功"));
}, ze = (r) => {
const d = e.get(r);
d && (fe(r), x(d.type), R(d.name), G(!0));
}, Be = () => {
$(ue), G(!1), B.success(`${te === "file" ? "文件" : "文件夹"}已删除`);
}, _e = M(
(r, d) => {
const c = d || "root";
switch (r) {
case "newFile":
se("file", c);
break;
case "newFolder":
se("folder", c);
break;
case "rename":
c !== "root" && We(c);
break;
case "copy":
c !== "root" && (V(c), B.success("已复制到剪贴板"));
break;
case "paste":
ge(c), B.success("粘贴成功");
break;
case "delete":
c !== "root" && ze(c);
break;
}
p((m) => ({ ...m, show: !1 }));
},
[V, ge]
), Ve = M((r, d) => {
r.preventDefault(), p({
x: r.clientX,
y: r.clientY,
targetId: d,
show: !0
});
}, []), qe = M(() => {
p((r) => ({ ...r, show: !1 }));
}, []), Ke = () => {
F.current = !0;
const r = (c) => {
if (!F.current) return;
const m = Math.max(200, Math.min(c.clientX, window.innerWidth - 200));
w(m);
}, d = () => {
F.current = !1, s && oe(s), document.removeEventListener("mousemove", r), document.removeEventListener("mouseup", d);
};
document.addEventListener("mousemove", r), document.addEventListener("mouseup", d);
}, pe = s ? e.get(s) : null, ye = o.size > 0, Ue = [
{
key: "newFile",
label: "新建文件",
icon: /* @__PURE__ */ l(be, {})
},
{
key: "newFolder",
label: "新建文件夹",
icon: /* @__PURE__ */ l(ve, {})
},
{ type: "divider" },
{
key: "rename",
label: "重命名",
icon: /* @__PURE__ */ l(st, {}),
disabled: u.targetId === "root"
},
{
key: "copy",
label: "复制",
icon: /* @__PURE__ */ l(it, {}),
disabled: u.targetId === "root"
},
{
key: "paste",
label: "粘贴",
icon: /* @__PURE__ */ l(lt, {}),
disabled: !h
},
{ type: "divider" },
{
key: "delete",
label: "删除",
icon: /* @__PURE__ */ l(at, {}),
danger: !0,
disabled: u.targetId === "root"
}
];
return /* @__PURE__ */ k("div", { className: "code-editor", onClick: qe, children: [
/* @__PURE__ */ k("div", { className: "editor-container", children: [
/* @__PURE__ */ k("div", { className: "file-panel", style: { width: T }, children: [
/* @__PURE__ */ k("div", { className: "file-panel-header", children: [
/* @__PURE__ */ l("h3", { children: "文件资源管理器" }),
/* @__PURE__ */ l("div", { className: "toolbar", children: /* @__PURE__ */ k(Y, { size: "small", children: [
/* @__PURE__ */ l(xe, { title: "新建文件", children: /* @__PURE__ */ l(be, { onClick: () => se("file", "root") }) }),
/* @__PURE__ */ l(xe, { title: "新建文件夹", children: /* @__PURE__ */ l(ve, { onClick: () => se("folder", "root") }) })
] }) })
] }),
/* @__PURE__ */ l(
Ft,
{
fileSystem: e,
onFileClick: we,
onFolderToggle: Re,
onContextMenu: Ve,
activeFileId: s,
openFiles: o
}
)
] }),
/* @__PURE__ */ l("div", { className: "resizer", onMouseDown: Ke }),
/* @__PURE__ */ k("div", { className: "editor-panel", children: [
ye && /* @__PURE__ */ l(
St,
{
openFiles: o,
fileSystem: e,
activeFileId: s,
onTabClick: f,
onTabClose: De
}
),
/* @__PURE__ */ l("div", { className: "monaco-container", children: ye ? /* @__PURE__ */ l(
mt,
{
height: "100%",
theme: "vs-dark",
language: (pe == null ? void 0 : pe.language) || "javascript",
onMount: Le,
options: {
automaticLayout: !0,
fontSize: 14,
wordWrap: "on",
minimap: { enabled: !0 },
scrollBeyondLastLine: !1,
renderLineHighlight: "all",
selectOnLineNumbers: !0,
tabSize: 2,
insertSpaces: !0
}
}
) : /* @__PURE__ */ k("div", { className: "welcome-screen", children: [
/* @__PURE__ */ l("h2", { children: "简陋的代码编辑器" }),
/* @__PURE__ */ l("p", { children: "选择一个文件开始编辑,或创建一个新文件" })
] }) })
] })
] }),
u.show && /* @__PURE__ */ l(
pt,
{
open: u.show,
menu: {
items: Ue,
onClick: ({ key: r }) => _e(r, u.targetId)
},
trigger: ["contextMenu"],
overlayStyle: {
position: "fixed",
left: `${u.x}px`,
top: `${u.y}px`
},
children: /* @__PURE__ */ l("div", { style: { position: "fixed", left: u.x, top: u.y, width: 0, height: 0 } })
}
),
/* @__PURE__ */ l(
he,
{
title: `新建${E === "file" ? "文件" : "文件夹"}`,
open: y,
onOk: Ae,
onCancel: () => b(!1),
okText: "确定",
cancelText: "取消",
children: /* @__PURE__ */ l(
Ce,
{
placeholder: `请输入${E === "file" ? "文件" : "文件夹"}名称`,
value: U,
onChange: (r) => ne(r.target.value),
autoFocus: !0
}
)
}
),
/* @__PURE__ */ l(
he,
{
title: "重命名",
open: ce,
onOk: Ne,
onCancel: () => H(!1),
okText: "确定",
cancelText: "取消",
children: /* @__PURE__ */ l(
Ce,
{
placeholder: "请输入新名称",
value: Q,
onChange: (r) => re(r.target.value),
autoFocus: !0
}
)
}
),
/* @__PURE__ */ l(
he,
{
title: "确认删除",
open: de,
onOk: Be,
onCancel: () => G(!1),
okText: "确定",
cancelText: "取消",
children: /* @__PURE__ */ k("p", { children: [
"确定要删除",
te === "folder" ? "文件夹" : "文件",
' "',
j,
'"',
te === "folder" ? "及其所有内容" : "",
"吗?"
] })
}
)
] });
};
export {
Ht as CodeEditor,
Kt as EndSwitch,
Vt as ErrorFix,
qt as Gallery,
Bt as Preview,
Ut as SessionLayout,
zt as SessionProvider,
_t as TerminalPanel,
Nt as WebContainerProvider,
Z as useSession,
ae as useWebContainer
};
//# sourceMappingURL=index.esm.js.map