diff --git a/.codesandbox/tasks.json b/.codesandbox/tasks.json index fa65446e2..0461e480c 100644 --- a/.codesandbox/tasks.json +++ b/.codesandbox/tasks.json @@ -63,6 +63,10 @@ "pr-link": "direct" } }, + "dev:nextjs": { + "name": "Dev: Nextjs example", + "command": "yarn dev:nextjs" + }, "dev:website-landing": { "name": "Dev: Website landing", "command": "yarn dev:landing" diff --git a/.prettierignore b/.prettierignore index 4b6850b40..52ffa1d05 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,23 +1,9 @@ -!/standalone-packages/vscode/src/vs/codesandbox/ -!/standalone-packages/vscode/src/vs/codesandbox/* -!/standalone-packages/vscode/src/vs/codesandbox/**/*.ts -!/standalone-packages/vscode/src/vs/codesandbox/**/*.* - dist/ lib/ -packages/executors/.rts2_cache_cjs -packages/executors/.rts2_cache_es -packages/executors/.rts2_cache_umd -packages/homepage/.cache -packages/homepage/public -static/ -/standalone-packages/* -www/ + # Our server side code changes html, we don't want to unintentionally break this by formatting *.html .drone.yml -!/standalone-packages/react-sandpack -!/standalone-packages/sandpack \ No newline at end of file diff --git a/examples/nextjs-app-dir/app/api/sandbox/[id]/route.ts b/examples/nextjs-app-dir/app/api/sandbox/[id]/route.ts new file mode 100644 index 000000000..6b2f71f29 --- /dev/null +++ b/examples/nextjs-app-dir/app/api/sandbox/[id]/route.ts @@ -0,0 +1,22 @@ +import { CodeSandbox } from "@codesandbox/sdk"; + +type Params = { params: { id: string } }; + +const apiKey = process.env.CSB_API_TOKEN as string; +const sdk = new CodeSandbox(apiKey, {}); + +export const GET = async (_req: Request, { params }: Params) => { + const templateId = params.id; + const data = await sdk.sandbox.start(templateId); + + return new Response(JSON.stringify(data), { status: 200 }); +}; + +export const POST = async (_req: Request, { params }: Params) => { + const templateId = params.id; + + const sandbox = await sdk.sandbox.create({ template: templateId }); + const data = await sdk.sandbox.start(sandbox.id); + + return new Response(JSON.stringify(data), { status: 200 }); +}; diff --git a/examples/nextjs-app-dir/components/sandpack-examples.tsx b/examples/nextjs-app-dir/components/sandpack-examples.tsx index 710f03599..f43f46ac1 100644 --- a/examples/nextjs-app-dir/components/sandpack-examples.tsx +++ b/examples/nextjs-app-dir/components/sandpack-examples.tsx @@ -1,16 +1,52 @@ -import { Sandpack } from "@codesandbox/sandpack-react"; -import { githubLight, sandpackDark } from "@codesandbox/sandpack-themes"; +"use client"; +import { + Sandpack, + SandpackCodeEditor, + SandpackFileExplorer, + SandpackLayout, + SandpackPreview, + SandpackProvider, +} from "@codesandbox/sandpack-react"; +import { useState } from "react"; + +const TEMPLATES = ["vite-react-ts", "nextjs", "rust", "python", "node"]; + /** * The only reason this is a separate import, is so * we don't need to make the full page 'use client', but only this copmponent. */ export const SandpackExamples = () => { + const [state, setState] = useState( + window.localStorage["template"] || TEMPLATES[0] + ); + return ( <> - - - - + + + `/api/sandbox/${id}`, + }} + > + + + + + + ); }; diff --git a/examples/nextjs-app-dir/next-env.d.ts b/examples/nextjs-app-dir/next-env.d.ts index 4f11a03dc..40c3d6809 100644 --- a/examples/nextjs-app-dir/next-env.d.ts +++ b/examples/nextjs-app-dir/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/lerna.json b/lerna.json index f47d3a430..d083882ff 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,10 @@ { - "packages": ["sandpack-client", "sandpack-react"], + "packages": [ + "sandpack-client", + "sandpack-react", + "playgorund", + "sandpack-environments" + ], "npmClient": "yarn", "command": { "version": { diff --git a/package.json b/package.json index d1eaf64bd..5d9f5b0ef 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,19 @@ "version": "1.0.0", "private": true, "workspaces": [ + "playground", "sandpack-client", "sandpack-react", "sandpack-themes", + "sandpack-environments", + "examples/nextjs-app-dir", "website/*" ], "nohoist": [ "website/docs/**", - "**/html-minifier-terser" + "**/html-minifier-terser", + "**/static-browser-server", + "**/static-browser-server/**" ], "description": "", "scripts": { @@ -26,7 +31,8 @@ "dev:docs": "yarn workspace sandpack-docs dev", "dev:react": "turbo run dev --filter=@codesandbox/sandpack-react --filter=@codesandbox/sandpack-client", "dev:landing": "yarn workspace sandpack-landing dev -p 3001", - "dev:theme": "yarn workspace sandpack-theme dev -p 3002" + "dev:theme": "yarn workspace sandpack-theme dev -p 3002", + "dev:nextjs": "yarn workspace nextjs-app-dir dev" }, "repository": { "type": "git", @@ -38,11 +44,11 @@ "@babel/preset-env": "^7.16.5", "@babel/preset-react": "^7.16.5", "@babel/preset-typescript": "^7.16.5", - "@rollup/plugin-commonjs": "^24.0.0", - "@rollup/plugin-node-resolve": "^15.0.1", - "@rollup/plugin-replace": "^5.0.2", - "@rollup/plugin-terser": "^0.4.0", - "@rollup/plugin-typescript": "^10.0.1", + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-replace": "^6.0.2", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.2", "@types/jest": "^27.4.0", "@typescript-eslint/eslint-plugin": "^6.9.0", "@typescript-eslint/parser": "^6.9.0", @@ -61,7 +67,7 @@ "lint-staged": "^10.5.4", "prettier": "^2.2.1", "react-test-renderer": "^18.1.0", - "rollup": "^3.9.1", + "rollup": "^4.39.0", "rollup-plugin-string": "^3.0.0", "turbo": "^1.5.5" }, diff --git a/playground/.gitignore b/playground/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/playground/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/playground/README.md b/playground/README.md new file mode 100644 index 000000000..40ede56ea --- /dev/null +++ b/playground/README.md @@ -0,0 +1,54 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config({ + extends: [ + // Remove ...tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config({ + plugins: { + // Add the react-x and react-dom plugins + 'react-x': reactX, + 'react-dom': reactDom, + }, + rules: { + // other rules... + // Enable its recommended typescript rules + ...reactX.configs['recommended-typescript'].rules, + ...reactDom.configs.recommended.rules, + }, +}) +``` diff --git a/playground/index.html b/playground/index.html new file mode 100644 index 000000000..e4b78eae1 --- /dev/null +++ b/playground/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/playground/package.json b/playground/package.json new file mode 100644 index 000000000..a4b0f24b5 --- /dev/null +++ b/playground/package.json @@ -0,0 +1,27 @@ +{ + "name": "@codesandbox/playground", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "dev:server": "vite-node server/index.ts", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@codesandbox/sandpack-react": "workspace:*", + "express": "^5.1.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router": "^7.5.0" + }, + "devDependencies": { + "@types/react": "^18.0.15", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "globals": "^15.15.0", + "typescript": "~5.7.2", + "vite": "^6.2.0", + "vite-node": "^3.1.1" + } +} diff --git a/playground/public/vite.svg b/playground/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/playground/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground/server/index.ts b/playground/server/index.ts new file mode 100644 index 000000000..455051a15 --- /dev/null +++ b/playground/server/index.ts @@ -0,0 +1,137 @@ +import path from "path"; + +import type { + BundlerSandbox, + SandboxFiles, + StaticSandbox, + VMSandbox, +} from "@codesandbox/sandpack-react"; +import { CodeSandbox } from "@codesandbox/sdk"; +import dotenv from "dotenv"; +import express from "express"; + +// Load environment variables +dotenv.config(); + +const app = express(); +const port = 3001; + +// Middleware to parse JSON bodies +app.use(express.json()); + +// Serve static files from the 'public' directory +const publicPath = path.join(__dirname, "public"); +app.use(express.static(publicPath)); + +const apiKey = JSON.parse(process.env.CSB_API_KEY) as string; +const globalApiKey = JSON.parse(process.env.CSB_GLOBAL_API_KEY) as string; + +const sdk = new CodeSandbox(apiKey, {}); + +// TODO: Deliver the actual built files +app.get("/", async (req, res) => { + res.send("Hello from CodeSandbox API!"); +}); + +// GET endpoint for starting a sandbox from a template +app.get("/api/sandboxes/:id", async (req, res) => { + try { + const sandboxId = req.params.id; + const { data } = await fetch( + "https://codesandbox.io/api/v1/sandboxes/" + sandboxId, + { + method: "GET", + headers: { + Authorization: `Bearer ${globalApiKey}`, + "Content-Type": "application/json", + }, + } + ).then((res) => res.json()); + + if (data.v2) { + const session = await sdk.sandbox.start(sandboxId); + const sandbox: VMSandbox = { + environment: "vm", + session, + sandboxId, + }; + + res.status(200).json(sandbox); + return; + } + + const getModulePath = (moduleOrDirectory) => { + const parentDir = moduleOrDirectory.directory_shortid + ? data.directories.find( + (dir) => dir.shortid === moduleOrDirectory.directory_shortid + ) + : null; + return parentDir + ? getModulePath(parentDir).concat(moduleOrDirectory.title) + : [moduleOrDirectory.title]; + }; + + const entry = data.entry; + const files = data.modules.reduce((acc: SandboxFiles, module) => { + acc[getModulePath(module).join("/")] = { + code: module.code, + metadata: { shortid: module.shortid }, + }; + + return acc; + }, {}); + + const sandbox: BundlerSandbox | StaticSandbox = + data.template === "static" + ? { + environment: "static", + + entry, + files, + sandboxId, + } + : { + environment: "bundler", + entry, + files, + bundler: data.template, + sandboxId, + }; + + res.status(200).json(sandbox); + } catch (error) { + console.error("Error starting sandbox:", error); + res.status(500).json({ error: "Failed to start sandbox" }); + } +}); + +// POST endpoint for updating/creating a file in a sandbox +app.post("/api/sandboxes/:id/fs", async (req, res) => { + const sandboxId = req.params.id; + const { shortid, content } = req.body; + + // Implementation details to be handled by you + // Example: update file at 'path' with 'content' in sandbox 'sandboxId' + + await fetch( + `https://codesandbox.io/api/v1/sandboxes/${sandboxId}/modules/${shortid}`, + { + method: "PUT", + headers: { + Authorization: `Bearer ${globalApiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ module: { code: content } }), + } + ); + + res + .status(200) + .json({ message: "File update endpoint hit", sandboxId, shortid, content }); +}); + +// Start the server +app.listen(port, () => { + console.log(`Server running at http://localhost:${port}`); + console.log(`Serving static files from: ${publicPath}`); +}); diff --git a/playground/src/App.tsx b/playground/src/App.tsx new file mode 100644 index 000000000..24ecfd2b9 --- /dev/null +++ b/playground/src/App.tsx @@ -0,0 +1,15 @@ +import { Route, Routes } from "react-router"; + +import { Dashboard } from "./Dashboard"; +import { Editor } from "./Editor"; + +function App() { + return ( + + } path="/" /> + } path="/:sandboxId" /> + + ); +} + +export default App; diff --git a/playground/src/Dashboard.tsx b/playground/src/Dashboard.tsx new file mode 100644 index 000000000..4bd761092 --- /dev/null +++ b/playground/src/Dashboard.tsx @@ -0,0 +1,78 @@ +import { + SandpackCodeEditor, + SandpackFileExplorer, + SandpackLayout, + SandpackPreview, + SandpackProvider, +} from "@codesandbox/sandpack-react"; + +export function Dashboard() { + // Local + /* + return ( +

Hello World

', + }, + }, + entry: "./index.html", + }} + > + + + + + +
+ ); + */ + // Persisted Sandbox + /* + return ( + { + if (event.type === "update") { + return fetch("/api/sandboxes/hqw3k9/fs", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + path: event.path, + content: event.content, + shortid: event.metadata.shortid, + }), + }).then((res) => + res.ok + ? console.log("Sandbox updated") + : console.error("Error updating sandbox") + ); + } + }} + sandbox={() => fetch(`/api/sandboxes/hqw3k9`).then((res) => res.json())} + > + + + + + + + ); +*/ + // VM + return ( + fetch(`/api/sandboxes/qc7lnq`).then((res) => res.json())} + > + + + + + + + ); +} diff --git a/playground/src/Editor.tsx b/playground/src/Editor.tsx new file mode 100644 index 000000000..30ff9c586 --- /dev/null +++ b/playground/src/Editor.tsx @@ -0,0 +1,55 @@ +import { + SandpackCodeEditor, + SandpackFileExplorer, + SandpackLayout, + SandpackPreview, + SandpackProvider, +} from "@codesandbox/sandpack-react"; +import { useState } from "react"; +import { useParams } from "react-router"; + +const TEMPLATES = ["vite-react-ts", "nextjs", "rust", "python", "node"]; + +/** + * The only reason this is a separate import, is so + * we don't need to make the full page 'use client', but only this copmponent. + */ +export function Editor() { + const { sandboxId } = useParams(); + const [state, setState] = useState( + window.localStorage["template"] || TEMPLATES[0] + ); + + return ( + <> + + + `/api/sandbox/${id}`, + }} + template={state} + > + + + {/**/} + + + + + ); +} diff --git a/playground/src/main.tsx b/playground/src/main.tsx new file mode 100644 index 000000000..4226ad529 --- /dev/null +++ b/playground/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router"; + +import App from "./App.tsx"; + +createRoot(document.getElementById("root")!).render( + + + + + +); diff --git a/playground/src/vite-env.d.ts b/playground/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/playground/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/playground/tsconfig.app.json b/playground/tsconfig.app.json new file mode 100644 index 000000000..358ca9ba9 --- /dev/null +++ b/playground/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/playground/tsconfig.json b/playground/tsconfig.json new file mode 100644 index 000000000..1ffef600d --- /dev/null +++ b/playground/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/playground/tsconfig.node.json b/playground/tsconfig.node.json new file mode 100644 index 000000000..db0becc8b --- /dev/null +++ b/playground/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/playground/vite.config.ts b/playground/vite.config.ts new file mode 100644 index 000000000..a093789e4 --- /dev/null +++ b/playground/vite.config.ts @@ -0,0 +1,23 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + "/api": { + target: "http://localhost:3001", + changeOrigin: true, + secure: false, + }, + }, + }, + define: { + // Provide a shim for `process.env` + "process.env": { + CSB_API_KEY: JSON.stringify(process.env.CSB_API_KEY), + CSB_GLOBAL_API_KEY: JSON.stringify(process.env.CSB_GLOBAL_API_KEY), + }, + }, +}); diff --git a/playground/yarn.lock b/playground/yarn.lock new file mode 100644 index 000000000..671d7a2a1 --- /dev/null +++ b/playground/yarn.lock @@ -0,0 +1,1211 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/compat-data@^7.26.8": + version "7.26.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367" + integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ== + +"@babel/core@^7.26.0": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.10.tgz#5c876f83c8c4dcb233ee4b670c0606f2ac3000f9" + integrity sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.10" + "@babel/helper-compilation-targets" "^7.26.5" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helpers" "^7.26.10" + "@babel/parser" "^7.26.10" + "@babel/template" "^7.26.9" + "@babel/traverse" "^7.26.10" + "@babel/types" "^7.26.10" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.26.10", "@babel/generator@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.0.tgz#764382b5392e5b9aff93cadb190d0745866cbc2c" + integrity sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw== + dependencies: + "@babel/parser" "^7.27.0" + "@babel/types" "^7.27.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.26.5": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz#de0c753b1cd1d9ab55d473c5a5cf7170f0a81880" + integrity sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA== + dependencies: + "@babel/compat-data" "^7.26.8" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-module-imports@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-module-transforms@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" + integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/helper-plugin-utils@^7.25.9": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35" + integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg== + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== + +"@babel/helpers@^7.26.10": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.0.tgz#53d156098defa8243eab0f32fa17589075a1b808" + integrity sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg== + dependencies: + "@babel/template" "^7.27.0" + "@babel/types" "^7.27.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.26.10", "@babel/parser@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.0.tgz#3d7d6ee268e41d2600091cbd4e145ffee85a44ec" + integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg== + dependencies: + "@babel/types" "^7.27.0" + +"@babel/plugin-transform-react-jsx-self@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz#c0b6cae9c1b73967f7f9eb2fca9536ba2fad2858" + integrity sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-react-jsx-source@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz#4c6b8daa520b5f155b5fb55547d7c9fa91417503" + integrity sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/template@^7.26.9", "@babel/template@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4" + integrity sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/parser" "^7.27.0" + "@babel/types" "^7.27.0" + +"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.0.tgz#11d7e644779e166c0442f9a07274d02cd91d4a70" + integrity sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.27.0" + "@babel/parser" "^7.27.0" + "@babel/template" "^7.27.0" + "@babel/types" "^7.27.0" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.10", "@babel/types@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.0.tgz#ef9acb6b06c3173f6632d993ecb6d4ae470b4559" + integrity sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@esbuild/aix-ppc64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz#b87036f644f572efb2b3c75746c97d1d2d87ace8" + integrity sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag== + +"@esbuild/android-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz#5ca7dc20a18f18960ad8d5e6ef5cf7b0a256e196" + integrity sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w== + +"@esbuild/android-arm@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.2.tgz#3c49f607b7082cde70c6ce0c011c362c57a194ee" + integrity sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA== + +"@esbuild/android-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.2.tgz#8a00147780016aff59e04f1036e7cb1b683859e2" + integrity sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg== + +"@esbuild/darwin-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz#486efe7599a8d90a27780f2bb0318d9a85c6c423" + integrity sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA== + +"@esbuild/darwin-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz#95ee222aacf668c7a4f3d7ee87b3240a51baf374" + integrity sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA== + +"@esbuild/freebsd-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz#67efceda8554b6fc6a43476feba068fb37fa2ef6" + integrity sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w== + +"@esbuild/freebsd-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz#88a9d7ecdd3adadbfe5227c2122d24816959b809" + integrity sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ== + +"@esbuild/linux-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz#87be1099b2bbe61282333b084737d46bc8308058" + integrity sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g== + +"@esbuild/linux-arm@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz#72a285b0fe64496e191fcad222185d7bf9f816f6" + integrity sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g== + +"@esbuild/linux-ia32@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz#337a87a4c4dd48a832baed5cbb022be20809d737" + integrity sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ== + +"@esbuild/linux-loong64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz#1b81aa77103d6b8a8cfa7c094ed3d25c7579ba2a" + integrity sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w== + +"@esbuild/linux-mips64el@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz#afbe380b6992e7459bf7c2c3b9556633b2e47f30" + integrity sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q== + +"@esbuild/linux-ppc64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz#6bf8695cab8a2b135cca1aa555226dc932d52067" + integrity sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g== + +"@esbuild/linux-riscv64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz#43c2d67a1a39199fb06ba978aebb44992d7becc3" + integrity sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw== + +"@esbuild/linux-s390x@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz#419e25737ec815c6dce2cd20d026e347cbb7a602" + integrity sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q== + +"@esbuild/linux-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz#22451f6edbba84abe754a8cbd8528ff6e28d9bcb" + integrity sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg== + +"@esbuild/netbsd-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz#744affd3b8d8236b08c5210d828b0698a62c58ac" + integrity sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw== + +"@esbuild/netbsd-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz#dbbe7521fd6d7352f34328d676af923fc0f8a78f" + integrity sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg== + +"@esbuild/openbsd-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz#f9caf987e3e0570500832b487ce3039ca648ce9f" + integrity sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg== + +"@esbuild/openbsd-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz#d2bb6a0f8ffea7b394bb43dfccbb07cabd89f768" + integrity sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw== + +"@esbuild/sunos-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz#49b437ed63fe333b92137b7a0c65a65852031afb" + integrity sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA== + +"@esbuild/win32-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz#081424168463c7d6c7fb78f631aede0c104373cf" + integrity sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q== + +"@esbuild/win32-ia32@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz#3f9e87143ddd003133d21384944a6c6cadf9693f" + integrity sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg== + +"@esbuild/win32-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz#839f72c2decd378f86b8f525e1979a97b920c67d" + integrity sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA== + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@rollup/rollup-android-arm-eabi@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.39.0.tgz#1d8cc5dd3d8ffe569d8f7f67a45c7909828a0f66" + integrity sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA== + +"@rollup/rollup-android-arm64@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.39.0.tgz#9c136034d3d9ed29d0b138c74dd63c5744507fca" + integrity sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ== + +"@rollup/rollup-darwin-arm64@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.39.0.tgz#830d07794d6a407c12b484b8cf71affd4d3800a6" + integrity sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q== + +"@rollup/rollup-darwin-x64@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.39.0.tgz#b26f0f47005c1fa5419a880f323ed509dc8d885c" + integrity sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ== + +"@rollup/rollup-freebsd-arm64@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.39.0.tgz#2b60c81ac01ff7d1bc8df66aee7808b6690c6d19" + integrity sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ== + +"@rollup/rollup-freebsd-x64@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.39.0.tgz#4826af30f4d933d82221289068846c9629cc628c" + integrity sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q== + +"@rollup/rollup-linux-arm-gnueabihf@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.39.0.tgz#a1f4f963d5dcc9e5575c7acf9911824806436bf7" + integrity sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g== + +"@rollup/rollup-linux-arm-musleabihf@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.39.0.tgz#e924b0a8b7c400089146f6278446e6b398b75a06" + integrity sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw== + +"@rollup/rollup-linux-arm64-gnu@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.39.0.tgz#cb43303274ec9a716f4440b01ab4e20c23aebe20" + integrity sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ== + +"@rollup/rollup-linux-arm64-musl@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.39.0.tgz#531c92533ce3d167f2111bfcd2aa1a2041266987" + integrity sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA== + +"@rollup/rollup-linux-loongarch64-gnu@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.39.0.tgz#53403889755d0c37c92650aad016d5b06c1b061a" + integrity sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw== + +"@rollup/rollup-linux-powerpc64le-gnu@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.39.0.tgz#f669f162e29094c819c509e99dbeced58fc708f9" + integrity sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ== + +"@rollup/rollup-linux-riscv64-gnu@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.39.0.tgz#4bab37353b11bcda5a74ca11b99dea929657fd5f" + integrity sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ== + +"@rollup/rollup-linux-riscv64-musl@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.39.0.tgz#4d66be1ce3cfd40a7910eb34dddc7cbd4c2dd2a5" + integrity sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA== + +"@rollup/rollup-linux-s390x-gnu@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.39.0.tgz#7181c329395ed53340a0c59678ad304a99627f6d" + integrity sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA== + +"@rollup/rollup-linux-x64-gnu@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.39.0.tgz#00825b3458094d5c27cb4ed66e88bfe9f1e65f90" + integrity sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA== + +"@rollup/rollup-linux-x64-musl@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.39.0.tgz#81caac2a31b8754186f3acc142953a178fcd6fba" + integrity sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg== + +"@rollup/rollup-win32-arm64-msvc@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.39.0.tgz#3a3f421f5ce9bd99ed20ce1660cce7cee3e9f199" + integrity sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ== + +"@rollup/rollup-win32-ia32-msvc@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.39.0.tgz#a44972d5cdd484dfd9cf3705a884bf0c2b7785a7" + integrity sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ== + +"@rollup/rollup-win32-x64-msvc@4.39.0": + version "4.39.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.39.0.tgz#bfe0214e163f70c4fec1c8f7bb8ce266f4c05b7e" + integrity sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug== + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.7.tgz#968cdc2366ec3da159f61166428ee40f370e56c2" + integrity sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng== + dependencies: + "@babel/types" "^7.20.7" + +"@types/estree@1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + +"@types/react-dom@^19.0.4": + version "19.1.2" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.2.tgz#bd1fe3b8c28a3a2e942f85314dcfb71f531a242f" + integrity sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw== + +"@types/react@^19.0.10": + version "19.1.0" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.0.tgz#73c43ad9bc43496ca8184332b111e2aef63fc9da" + integrity sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w== + dependencies: + csstype "^3.0.2" + +"@vitejs/plugin-react@^4.3.4": + version "4.3.4" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz#c64be10b54c4640135a5b28a2432330e88ad7c20" + integrity sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug== + dependencies: + "@babel/core" "^7.26.0" + "@babel/plugin-transform-react-jsx-self" "^7.25.9" + "@babel/plugin-transform-react-jsx-source" "^7.25.9" + "@types/babel__core" "^7.20.5" + react-refresh "^0.14.2" + +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + +body-parser@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa" + integrity sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg== + dependencies: + bytes "^3.1.2" + content-type "^1.0.5" + debug "^4.4.0" + http-errors "^2.0.0" + iconv-lite "^0.6.3" + on-finished "^2.4.1" + qs "^6.14.0" + raw-body "^3.0.0" + type-is "^2.0.0" + +browserslist@^4.24.0: + version "4.24.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" + integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== + dependencies: + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" + update-browserslist-db "^1.1.1" + +bytes@3.1.2, bytes@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +caniuse-lite@^1.0.30001688: + version "1.0.30001713" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz#6b33a8857e6c7dcb41a0caa2dd0f0489c823a52d" + integrity sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q== + +content-disposition@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" + integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg== + dependencies: + safe-buffer "5.2.1" + +content-type@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + +cookie@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +debug@^4.1.0, debug@^4.3.1, debug@^4.3.5, debug@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + +depd@2.0.0, depd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.73: + version "1.5.135" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.135.tgz#6d835020fa0c7f02f30d7608c2f3c0a764236699" + integrity sha512-8gXUdEmvb+WCaYUhA0Svr08uSeRjM2w3x5uHOc1QbaEVzJXB8rgm5eptieXzyKoVEtinLvW6MtTcurA65PeS1Q== + +encodeurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21" + integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +esbuild@^0.25.0: + version "0.25.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.2.tgz#55a1d9ebcb3aa2f95e8bba9e900c1a5061bc168b" + integrity sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.2" + "@esbuild/android-arm" "0.25.2" + "@esbuild/android-arm64" "0.25.2" + "@esbuild/android-x64" "0.25.2" + "@esbuild/darwin-arm64" "0.25.2" + "@esbuild/darwin-x64" "0.25.2" + "@esbuild/freebsd-arm64" "0.25.2" + "@esbuild/freebsd-x64" "0.25.2" + "@esbuild/linux-arm" "0.25.2" + "@esbuild/linux-arm64" "0.25.2" + "@esbuild/linux-ia32" "0.25.2" + "@esbuild/linux-loong64" "0.25.2" + "@esbuild/linux-mips64el" "0.25.2" + "@esbuild/linux-ppc64" "0.25.2" + "@esbuild/linux-riscv64" "0.25.2" + "@esbuild/linux-s390x" "0.25.2" + "@esbuild/linux-x64" "0.25.2" + "@esbuild/netbsd-arm64" "0.25.2" + "@esbuild/netbsd-x64" "0.25.2" + "@esbuild/openbsd-arm64" "0.25.2" + "@esbuild/openbsd-x64" "0.25.2" + "@esbuild/sunos-x64" "0.25.2" + "@esbuild/win32-arm64" "0.25.2" + "@esbuild/win32-ia32" "0.25.2" + "@esbuild/win32-x64" "0.25.2" + +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9" + integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA== + dependencies: + accepts "^2.0.0" + body-parser "^2.2.0" + content-disposition "^1.0.0" + content-type "^1.0.5" + cookie "^0.7.1" + cookie-signature "^1.2.1" + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + finalhandler "^2.1.0" + fresh "^2.0.0" + http-errors "^2.0.0" + merge-descriptors "^2.0.0" + mime-types "^3.0.0" + on-finished "^2.4.1" + once "^1.4.0" + parseurl "^1.3.3" + proxy-addr "^2.0.7" + qs "^6.14.0" + range-parser "^1.2.1" + router "^2.2.0" + send "^1.1.0" + serve-static "^2.2.0" + statuses "^2.0.1" + type-is "^2.0.1" + vary "^1.1.2" + +finalhandler@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.0.tgz#72306373aa89d05a8242ed569ed86a1bff7c561f" + integrity sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q== + dependencies: + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^15.15.0: + version "15.15.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8" + integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg== + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +http-errors@2.0.0, http-errors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +iconv-lite@0.6.3, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^3.0.0, mime-types@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce" + integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA== + dependencies: + mime-db "^1.54.0" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.8: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +on-finished@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +parseurl@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" + integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== + +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +picocolors@^1.0.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +postcss@^8.5.3: + version "8.5.3" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" + integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== + dependencies: + nanoid "^3.3.8" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +proxy-addr@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@^6.14.0: + version "6.14.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== + dependencies: + side-channel "^1.1.0" + +range-parser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" + integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.6.3" + unpipe "1.0.0" + +react-dom@^19.0.0: + version "19.1.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.0.tgz#133558deca37fa1d682708df8904b25186793623" + integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g== + dependencies: + scheduler "^0.26.0" + +react-refresh@^0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" + integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== + +react@^19.0.0: + version "19.1.0" + resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75" + integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== + +rollup@^4.30.1: + version "4.39.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.39.0.tgz#9dc1013b70c0e2cb70ef28350142e9b81b3f640c" + integrity sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g== + dependencies: + "@types/estree" "1.0.7" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.39.0" + "@rollup/rollup-android-arm64" "4.39.0" + "@rollup/rollup-darwin-arm64" "4.39.0" + "@rollup/rollup-darwin-x64" "4.39.0" + "@rollup/rollup-freebsd-arm64" "4.39.0" + "@rollup/rollup-freebsd-x64" "4.39.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.39.0" + "@rollup/rollup-linux-arm-musleabihf" "4.39.0" + "@rollup/rollup-linux-arm64-gnu" "4.39.0" + "@rollup/rollup-linux-arm64-musl" "4.39.0" + "@rollup/rollup-linux-loongarch64-gnu" "4.39.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.39.0" + "@rollup/rollup-linux-riscv64-gnu" "4.39.0" + "@rollup/rollup-linux-riscv64-musl" "4.39.0" + "@rollup/rollup-linux-s390x-gnu" "4.39.0" + "@rollup/rollup-linux-x64-gnu" "4.39.0" + "@rollup/rollup-linux-x64-musl" "4.39.0" + "@rollup/rollup-win32-arm64-msvc" "4.39.0" + "@rollup/rollup-win32-ia32-msvc" "4.39.0" + "@rollup/rollup-win32-x64-msvc" "4.39.0" + fsevents "~2.3.2" + +router@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== + dependencies: + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +scheduler@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337" + integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA== + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +send@^1.1.0, send@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/send/-/send-1.2.0.tgz#32a7554fb777b831dfa828370f773a3808d37212" + integrity sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw== + dependencies: + debug "^4.3.5" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.0" + mime-types "^3.0.1" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.1" + +serve-static@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.0.tgz#9c02564ee259bdd2251b82d659a2e7e1938d66f9" + integrity sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ== + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +statuses@2.0.1, statuses@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@^2.0.0, type-is@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" + integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== + dependencies: + content-type "^1.0.5" + media-typer "^1.1.0" + mime-types "^3.0.0" + +typescript@~5.7.2: + version "5.7.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e" + integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== + +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +vary@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vite-node@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.1.1.tgz#ad186c07859a6e5fca7c7f563e55fb11b16557bc" + integrity sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w== + dependencies: + cac "^6.7.14" + debug "^4.4.0" + es-module-lexer "^1.6.0" + pathe "^2.0.3" + vite "^5.0.0 || ^6.0.0" + +"vite@^5.0.0 || ^6.0.0", vite@^6.2.0: + version "6.2.6" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.2.6.tgz#7f0ccf2fdc0c1eda079ce258508728e2473d3f61" + integrity sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw== + dependencies: + esbuild "^0.25.0" + postcss "^8.5.3" + rollup "^4.30.1" + optionalDependencies: + fsevents "~2.3.3" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== diff --git a/sandpack-client/package.json b/sandpack-client/package.json index d7f2a3548..ebfaf59f1 100644 --- a/sandpack-client/package.json +++ b/sandpack-client/package.json @@ -19,11 +19,6 @@ "import": "./dist/clients/runtime/index.mjs", "require": "./dist/clients/runtime/index.js" }, - "./clients/node": { - "types": "./dist/clients/node/index.d.ts", - "import": "./dist/clients/node/index.mjs", - "require": "./dist/clients/node/index.js" - }, "./clients/static": { "types": "./dist/clients/static/index.d.ts", "import": "./dist/clients/static/index.mjs", @@ -45,7 +40,8 @@ "build:bundler": "gulp", "format": "prettier --write '**/*.{ts,tsx,js,jsx}'", "format:check": "prettier --check '**/*.{ts,tsx}'", - "dev": "yarn build -- --watch" + "dev": "yarn build -- --watch", + "typecheck": "tsc --noEmit --skipLibCheck" }, "files": [ "dist", @@ -55,7 +51,7 @@ "README.md" ], "dependencies": { - "@codesandbox/nodebox": "0.1.8", + "@codesandbox/sdk": "^0.11.1", "buffer": "^6.0.3", "dequal": "^2.0.2", "mime-db": "^1.52.0", diff --git a/sandpack-client/rollup.config.js b/sandpack-client/rollup.config.js index 6ac089098..e13214f86 100644 --- a/sandpack-client/rollup.config.js +++ b/sandpack-client/rollup.config.js @@ -29,7 +29,7 @@ const configs = [ { input: { index: "src/index.ts", - "clients/node/index": "src/clients/node/index.ts", + "clients/vm/index": "src/clients/vm/index.ts", "clients/runtime/index": "src/clients/runtime/index.ts", }, output: [ @@ -46,7 +46,10 @@ const configs = [ ], plugins: [ - typescript({ tsconfig: "./tsconfig.json" }), + typescript({ + tsconfig: "./tsconfig.json", + compilerOptions: { declaration: true, declarationDir: "dist" }, + }), string({ include: "**/dist/consoleHook.js" }), replace({ preventAssignment: true, @@ -61,7 +64,7 @@ const configs = [ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}), ...(pkg.peerDependencies || {}), - }), + }).concat("@codesandbox/sdk/browser"), }, ]; diff --git a/sandpack-client/src/clients/event-emitter.ts b/sandpack-client/src/clients/event-emitter.ts index f154f48e0..9ef6e433e 100644 --- a/sandpack-client/src/clients/event-emitter.ts +++ b/sandpack-client/src/clients/event-emitter.ts @@ -6,7 +6,7 @@ import type { export class EventEmitter { private listeners: Record = {}; - private listenersCount = 0; + public listenersCount = 0; readonly channelId: number = Math.floor(Math.random() * 1000000); diff --git a/sandpack-client/src/clients/index.ts b/sandpack-client/src/clients/index.ts index 12f0abc49..aa787d511 100644 --- a/sandpack-client/src/clients/index.ts +++ b/sandpack-client/src/clients/index.ts @@ -15,13 +15,14 @@ export async function loadSandpackClient( let Client; switch (template) { - case "node": - Client = await import("./node").then((m) => m.SandpackNode); - break; case "static": Client = await import("./static").then((m) => m.SandpackStatic); break; + case "vm": + Client = await import("./vm").then((m) => m.SandpackVM); + break; + default: Client = await import("./runtime").then((m) => m.SandpackRuntime); } diff --git a/sandpack-client/src/clients/node/client.utils.ts b/sandpack-client/src/clients/node/client.utils.ts deleted file mode 100644 index 8a5bfe8a8..000000000 --- a/sandpack-client/src/clients/node/client.utils.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { ShellCommandOptions } from "@codesandbox/nodebox/build/modules/shell"; -import { invariant } from "outvariant"; - -import type { SandpackBundlerFiles } from "../.."; -import { createError } from "../.."; - -import { tokenize, TokenType } from "./taskManager"; - -let counter = 0; - -export function generateRandomId() { - const now = Date.now(); - const randomNumber = Math.round(Math.random() * 10000); - const count = (counter += 1); - return (+`${now}${randomNumber}${count}`).toString(16); -} - -export const writeBuffer = (content: string | Uint8Array): Uint8Array => { - if (typeof content === "string") { - return new TextEncoder().encode(content); - } else { - return content; - } -}; - -export const readBuffer = (content: string | Uint8Array): string => { - if (typeof content === "string") { - return content; - } else { - return new TextDecoder().decode(content); - } -}; - -export const fromBundlerFilesToFS = ( - files: SandpackBundlerFiles -): Record => { - return Object.entries(files).reduce>( - (acc, [key, value]) => { - acc[key] = writeBuffer(value.code); - - return acc; - }, - {} - ); -}; - -/** - * Figure out which script it must run to start a server - */ -export const findStartScriptPackageJson = ( - packageJson: string -): [string, string[], ShellCommandOptions] => { - let scripts: Record = {}; - // TODO: support postinstall - const possibleKeys = ["dev", "start"]; - - try { - scripts = JSON.parse(packageJson).scripts; - } catch (e) { - throw createError( - "Could not parse package.json file: " + (e as Error).message - ); - } - - invariant( - scripts, - "Failed to start. Please provide a `start` or `dev` script on the package.json" - ); - - for (let index = 0; index < possibleKeys.length; index++) { - if (possibleKeys[index] in scripts) { - const script = possibleKeys[index]; - - const candidate = scripts[script]; - - let env = {}; - let command = ""; - const args: string[] = []; - - tokenize(candidate).forEach((item) => { - const commandNotFoundYet = command === ""; - - if (item.type === TokenType.EnvVar) { - env = item.value; - } - - if (item.type === TokenType.Command && commandNotFoundYet) { - command = item.value!; - } - - if ( - item.type === TokenType.Argument || - (!commandNotFoundYet && item.type === TokenType.Command) - ) { - args.push(item.value!); - } - - // TODO: support TokenType.AND, TokenType.OR, TokenType.PIPE - }); - - return [command, args, { env }]; - } - } - - throw createError( - "Failed to start. Please provide a `start` or `dev` script on the package.json" - ); -}; - -export const getMessageFromError = (error: Error | string): string => { - if (typeof error === "string") return error; - - if (typeof error === "object" && "message" in error) { - return error.message; - } - - return createError( - "The server could not be reached. Make sure that the node script is running and that a port has been started." - ); -}; diff --git a/sandpack-client/src/clients/node/index.ts b/sandpack-client/src/clients/node/index.ts deleted file mode 100644 index a12aadfd9..000000000 --- a/sandpack-client/src/clients/node/index.ts +++ /dev/null @@ -1,463 +0,0 @@ -/* eslint-disable no-console,@typescript-eslint/no-explicit-any,prefer-rest-params,@typescript-eslint/explicit-module-boundary-types */ - -import { PREVIEW_LOADED_MESSAGE_TYPE, Nodebox } from "@codesandbox/nodebox"; -import type { - FilesMap, - ShellProcess, - FSWatchEvent, -} from "@codesandbox/nodebox"; -import type { ShellCommandOptions } from "@codesandbox/nodebox/build/modules/shell"; - -import type { - ClientOptions, - ListenerFunction, - SandboxSetup, - UnsubscribeFunction, -} from "../.."; -import { nullthrows } from "../.."; -import { createError } from "../.."; -import { SandpackClient } from "../base"; -import { EventEmitter } from "../event-emitter"; - -import { - fromBundlerFilesToFS, - readBuffer, - findStartScriptPackageJson, - getMessageFromError, - writeBuffer, - generateRandomId, -} from "./client.utils"; -import { loadPreviewIframe, setPreviewIframeProperties } from "./iframe.utils"; -import { injectScriptToIframe } from "./inject-scripts"; -import type { SandpackNodeMessage } from "./types"; - -export class SandpackNode extends SandpackClient { - // General - private emitter: EventEmitter; - - // Nodebox - private emulatorIframe!: HTMLIFrameElement; - private emulator!: Nodebox; - private emulatorShellProcess: ShellProcess | undefined; - private emulatorCommand: [string, string[], ShellCommandOptions] | undefined; - private iframePreviewUrl: string | undefined; - private _modulesCache = new Map(); - private messageChannelId = generateRandomId(); - - // Public - public iframe!: HTMLIFrameElement; - - private _initPromise: Promise | null = null; - - constructor( - selector: string | HTMLIFrameElement, - sandboxInfo: SandboxSetup, - options: ClientOptions = {} - ) { - super(selector, sandboxInfo, { - ...options, - bundlerURL: options.bundlerURL, - }); - - this.emitter = new EventEmitter(); - - // Assign iframes - this.manageIframes(selector); - - // Init emulator - this.emulator = new Nodebox({ - iframe: this.emulatorIframe, - runtimeUrl: this.options.bundlerURL, - }); - - // Trigger initial compile - this.updateSandbox(sandboxInfo); - } - - // Initialize nodebox, should only ever be called once - private async _init(files: FilesMap): Promise { - await this.emulator.connect(); - - // 2. Setup - await this.emulator.fs.init(files); - - // 2.1 Other dependencies - await this.globalListeners(); - } - - /** - * It initializes the emulator and provide it with files, template and script to run - */ - private async compile(files: FilesMap): Promise { - try { - // 1. Init - this.status = "initializing"; - this.dispatch({ type: "start", firstLoad: true }); - if (!this._initPromise) { - this._initPromise = this._init(files); - } - await this._initPromise; - - this.dispatch({ type: "connected" }); - - // 3. Create, run task and assign preview - const { id: shellId } = await this.createShellProcessFromTask(files); - - // 4. Launch Preview - await this.createPreviewURLFromId(shellId); - await this.setLocationURLIntoIFrame(); - - // 5. Returns to consumer - this.dispatchDoneMessage(); - } catch (err) { - this.dispatch({ - type: "action", - action: "notification", - notificationType: "error", - title: getMessageFromError(err as Error), - }); - - this.dispatch({ type: "done", compilatonError: true }); - } - } - - /** - * It creates a new shell and run the starting task - */ - private async createShellProcessFromTask( - files: FilesMap - ): Promise<{ id: string }> { - const packageJsonContent = readBuffer(files["/package.json"]); - - this.emulatorCommand = findStartScriptPackageJson(packageJsonContent); - this.emulatorShellProcess = this.emulator.shell.create(); - - // Shell listeners - await this.emulatorShellProcess.on("exit", (exitCode) => { - this.dispatch({ - type: "action", - action: "notification", - notificationType: "error", - title: createError(`Error: process.exit(${exitCode}) called.`), - }); - }); - - await this.emulatorShellProcess.on("progress", (data) => { - if ( - data.state === "command_running" || - data.state === "starting_command" - ) { - this.dispatch({ - type: "shell/progress", - data: { - ...data, - command: [ - this.emulatorCommand?.[0], - this.emulatorCommand?.[1].join(" "), - ].join(" "), - }, - }); - - this.status = "installing-dependencies"; - - return; - } - - this.dispatch({ type: "shell/progress", data }); - }); - - this.emulatorShellProcess.stdout.on("data", (data) => { - this.dispatch({ type: "stdout", payload: { data, type: "out" } }); - }); - - this.emulatorShellProcess.stderr.on("data", (data) => { - this.dispatch({ type: "stdout", payload: { data, type: "err" } }); - }); - - return await this.emulatorShellProcess.runCommand(...this.emulatorCommand); - } - - private async createPreviewURLFromId(id: string): Promise { - this.iframePreviewUrl = undefined; - - const { url } = await this.emulator.preview.getByShellId(id); - - this.iframePreviewUrl = url + (this.options.startRoute ?? ""); - } - - /** - * Nodebox needs to handle two types of iframes at the same time: - * - * 1. Runtime iframe: where the emulator process runs, which is responsible - * for creating the other iframes (hidden); - * 2. Preview iframes: any other node process that contains a PORT (public); - */ - private manageIframes(selector: string | HTMLIFrameElement): void { - /** - * Pick the preview iframe - */ - if (typeof selector === "string") { - const element = document.querySelector(selector); - - nullthrows(element, `The element '${selector}' was not found`); - - this.iframe = document.createElement("iframe"); - element?.appendChild(this.iframe); - } else { - this.iframe = selector; - } - - // Set preview iframe styles - setPreviewIframeProperties(this.iframe, this.options); - - nullthrows( - this.iframe.parentNode, - `The given iframe does not have a parent.` - ); - - /** - * Create the runtime iframe, which is hidden sibling - * from the preview one - */ - this.emulatorIframe = document.createElement("iframe"); - this.emulatorIframe.classList.add("sp-bridge-frame"); - this.iframe.parentNode?.appendChild(this.emulatorIframe); - } - - private async setLocationURLIntoIFrame(): Promise { - if (this.iframePreviewUrl) { - await loadPreviewIframe(this.iframe, this.iframePreviewUrl); - } - } - - /** - * Send all messages and events to tell to the - * consumer that the bundler is ready without any error - */ - private dispatchDoneMessage(): void { - this.status = "done"; - this.dispatch({ type: "done", compilatonError: false }); - - if (this.iframePreviewUrl) { - this.dispatch({ - type: "urlchange", - url: this.iframePreviewUrl, - back: false, - forward: false, - }); - } - } - - private async globalListeners(): Promise { - window.addEventListener("message", (event) => { - if (event.data.type === PREVIEW_LOADED_MESSAGE_TYPE) { - injectScriptToIframe(this.iframe, this.messageChannelId); - } - - if ( - event.data.type === "urlchange" && - event.data.channelId === this.messageChannelId - ) { - this.dispatch({ - type: "urlchange", - url: event.data.url, - back: event.data.back, - forward: event.data.forward, - }); - } else if (event.data.channelId === this.messageChannelId) { - this.dispatch(event.data); - } - }); - - await this.emulator.fs.watch( - ["*"], - [ - ".next", - "node_modules", - "build", - "dist", - "vendor", - ".config", - ".vuepress", - ], - - async (message) => { - if (!message) return; - - const event = message as FSWatchEvent; - - const path = - "newPath" in event - ? event.newPath - : "path" in event - ? event.path - : ""; - const { type } = await this.emulator.fs.stat(path); - if (type !== "file") return null; - - try { - switch (event.type) { - case "change": - case "create": { - const content = await this.emulator.fs.readFile( - event.path, - "utf8" - ); - this.dispatch({ - type: "fs/change", - path: event.path, - content: content, - }); - - this._modulesCache.set(event.path, writeBuffer(content)); - - break; - } - case "remove": - this.dispatch({ - type: "fs/remove", - path: event.path, - }); - - this._modulesCache.delete(event.path); - - break; - - case "rename": { - this.dispatch({ - type: "fs/remove", - path: event.oldPath, - }); - - this._modulesCache.delete(event.oldPath); - - const newContent = await this.emulator.fs.readFile( - event.newPath, - "utf8" - ); - this.dispatch({ - type: "fs/change", - path: event.newPath, - content: newContent, - }); - - this._modulesCache.set(event.newPath, writeBuffer(newContent)); - - break; - } - - case "close": - break; - } - } catch (err) { - this.dispatch({ - type: "action", - action: "notification", - notificationType: "error", - title: getMessageFromError(err as Error), - }); - } - } - ); - } - - /** - * PUBLIC Methods - */ - public async restartShellProcess(): Promise { - if (this.emulatorShellProcess && this.emulatorCommand) { - // 1. Set the loading state and clean the URL - this.dispatch({ type: "start", firstLoad: true }); - this.status = "initializing"; - - // 2. Exit shell - await this.emulatorShellProcess.kill(); - this.iframe?.removeAttribute("attr"); - - this.emulator.fs.rm("/node_modules/.vite", { - recursive: true, - force: true, - }); - - // 3 Run command again - await this.compile(Object.fromEntries(this._modulesCache)); - } - } - - public updateSandbox(setup: SandboxSetup): void { - const modules = fromBundlerFilesToFS(setup.files); - - /** - * Update file changes - */ - - if (this.emulatorShellProcess?.state === "running") { - Object.entries(modules).forEach(([key, value]) => { - if ( - !this._modulesCache.get(key) || - readBuffer(value) !== readBuffer(this._modulesCache.get(key)) - ) { - this.emulator.fs.writeFile(key, value, { recursive: true }); - } - }); - - return; - } - - /** - * Pass init files to the bundler - */ - this.dispatch({ - codesandbox: true, - modules, - template: setup.template, - type: "compile", - }); - - /** - * Add modules to cache, this will ensure uniqueness changes - * - * Keep it after the compile action, in order to update the cache at the right moment - */ - Object.entries(modules).forEach(([key, value]) => { - this._modulesCache.set(key, writeBuffer(value)); - }); - } - - public async dispatch(message: SandpackNodeMessage): Promise { - switch (message.type) { - case "compile": - this.compile(message.modules); - break; - - case "refresh": - await this.setLocationURLIntoIFrame(); - break; - - case "urlback": - case "urlforward": - this.iframe?.contentWindow?.postMessage(message, "*"); - break; - - case "shell/restart": - this.restartShellProcess(); - break; - - case "shell/openPreview": - window.open(this.iframePreviewUrl, "_blank"); - break; - - default: - this.emitter.dispatch(message); - } - } - - public listen(listener: ListenerFunction): UnsubscribeFunction { - return this.emitter.listener(listener); - } - - public destroy(): void { - this.emulatorIframe.remove(); - this.emitter.cleanup(); - } -} diff --git a/sandpack-client/src/clients/node/taskManager.test.ts b/sandpack-client/src/clients/node/taskManager.test.ts deleted file mode 100644 index fa595de81..000000000 --- a/sandpack-client/src/clients/node/taskManager.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { tokenize } from "./taskManager"; - -describe(tokenize, () => { - it("parses environment variables", () => { - const input = tokenize("FOO=1 tsc -p"); - const output = [ - { type: "EnvVar", value: { FOO: "1" } }, - { type: "Command", value: "tsc" }, - { type: "Argument", value: "-p" }, - ]; - - expect(input).toEqual(output); - }); - - it("parses multiples envs environment variables", () => { - const input = tokenize("FOO=1 BAZ=bla tsc -p"); - const output = [ - { type: "EnvVar", value: { FOO: "1", BAZ: "bla" } }, - { type: "Command", value: "tsc" }, - { type: "Argument", value: "-p" }, - ]; - - expect(input).toEqual(output); - }); - - it("parses command and argument", () => { - const input = tokenize("tsc -p"); - const output = [ - { type: "Command", value: "tsc" }, - { type: "Argument", value: "-p" }, - ]; - - expect(input).toEqual(output); - }); - - it("parses two commands", () => { - const input = tokenize("tsc && node"); - const output = [ - { type: "Command", value: "tsc" }, - { type: "AND" }, - { type: "Command", value: "node" }, - ]; - - expect(input).toEqual(output); - }); - - it("parses two commands", () => { - const input = tokenize("tsc -p . && node index.js"); - const output = [ - { type: "Command", value: "tsc" }, - { type: "Argument", value: "-p" }, - { type: "Command", value: "." }, - { type: "AND" }, - { type: "Command", value: "node" }, - { type: "Command", value: "index.js" }, - ]; - - expect(input).toEqual(output); - }); - - it("parses multiple arguments", () => { - const input = tokenize("tsc --foo -- --foo"); - const output = [ - { type: "Command", value: "tsc" }, - { type: "Argument", value: "--foo" }, - { type: "Argument", value: "--" }, - { type: "Argument", value: "--foo" }, - ]; - - expect(input).toEqual(output); - }); - - it("parses pipe and string commands", () => { - const input = tokenize(`echo "Hello World" | wc -w`); - const output = [ - { type: "Command", value: "echo" }, - { type: "String", value: '"Hello World"' }, - { type: "PIPE" }, - { type: "Command", value: "wc" }, - { type: "Argument", value: "-w" }, - ]; - - expect(input).toEqual(output); - }); - - it("parses escaped characters", () => { - const input = tokenize(`echo "Hello | World" | wc -w`); - const output = [ - { type: "Command", value: "echo" }, - { type: "String", value: '"Hello | World"' }, - { type: "PIPE" }, - { type: "Command", value: "wc" }, - { type: "Argument", value: "-w" }, - ]; - - expect(input).toEqual(output); - }); - - it("parses escaped characters", () => { - const input = tokenize(`echo "Hello | World" | wc -w`); - const output = [ - { type: "Command", value: "echo" }, - { type: "String", value: '"Hello | World"' }, - { type: "PIPE" }, - { type: "Command", value: "wc" }, - { type: "Argument", value: "-w" }, - ]; - - expect(input).toEqual(output); - }); - - it("parses or", () => { - const input = tokenize(`echo "Hello | World" || wc -w`); - const output = [ - { type: "Command", value: "echo" }, - { type: "String", value: '"Hello | World"' }, - { type: "OR" }, - { type: "Command", value: "wc" }, - { type: "Argument", value: "-w" }, - ]; - - expect(input).toEqual(output); - }); -}); diff --git a/sandpack-client/src/clients/node/taskManager.ts b/sandpack-client/src/clients/node/taskManager.ts deleted file mode 100644 index 93b1afe3b..000000000 --- a/sandpack-client/src/clients/node/taskManager.ts +++ /dev/null @@ -1,178 +0,0 @@ -function isCommand(char: string) { - return /[a-zA-Z.]/.test(char); -} - -function isAlpha(char: string) { - return /[a-zA-Z]/.test(char); -} - -function isWhitespace(char: string) { - return /\s/.test(char); -} - -function isOperator(char: string) { - return /[&|]/.test(char); -} - -function isArgument(char: string) { - return /-/.test(char); -} - -function isString(char: string) { - return /["']/.test(char); -} - -function isEnvVar(char: string) { - return isAlpha(char) && char === char.toUpperCase(); -} - -export enum TokenType { - OR = "OR", - AND = "AND", - PIPE = "PIPE", - Command = "Command", - Argument = "Argument", - String = "String", - EnvVar = "EnvVar", -} - -type Token = - | { type: TokenType.OR | TokenType.AND | TokenType.PIPE } - | { - type: TokenType.Command | TokenType.Argument | TokenType.String; - value?: string; - } - | { - type: TokenType.EnvVar; - value: Record; - }; - -const operators = new Map([ - ["&&", { type: TokenType.AND }], - ["||", { type: TokenType.OR }], - ["|", { type: TokenType.PIPE }], - ["-", { type: TokenType.Argument }], -]); - -export function tokenize(input: string): Token[] { - let current = 0; - const tokens = []; - - function parseCommand(): Token { - let value = ""; - while (isCommand(input[current]) && current < input.length) { - value += input[current]; - current++; - } - - return { type: TokenType.Command, value }; - } - - function parseOperator(): Token { - let value = ""; - while (isOperator(input[current]) && current < input.length) { - value += input[current]; - current++; - } - - return operators.get(value)!; - } - - function parseArgument(): Token { - let value = ""; - while ( - (isArgument(input[current]) || isAlpha(input[current])) && - current < input.length - ) { - value += input[current]; - current++; - } - - return { type: TokenType.Argument, value }; - } - - function parseString(): Token { - const openCloseQuote = input[current]; - - let value = input[current]; - current++; - - while (input[current] !== openCloseQuote && current < input.length) { - value += input[current]; - current++; - } - - value += input[current]; - current++; - - return { type: TokenType.String, value }; - } - - function parseEnvVars(): Token { - const value: Record = {}; - - const parseSingleEnv = () => { - let key = ""; - let pair = ""; - - while (input[current] !== "=" && current < input.length) { - key += input[current]; - current++; - } - - // Skip equal - if (input[current] === "=") { - current++; - } - - while (input[current] !== " " && current < input.length) { - pair += input[current]; - current++; - } - - value[key] = pair; - }; - - while (isEnvVar(input[current]) && current < input.length) { - parseSingleEnv(); - - current++; - } - - return { type: TokenType.EnvVar, value }; - } - - while (current < input.length) { - const currentChar = input[current]; - - if (isWhitespace(currentChar)) { - current++; - continue; - } - - switch (true) { - case isEnvVar(currentChar): - tokens.push(parseEnvVars()); - break; - - case isCommand(currentChar): - tokens.push(parseCommand()); - break; - case isOperator(currentChar): - tokens.push(parseOperator()); - break; - case isArgument(currentChar): - tokens.push(parseArgument()); - break; - - case isString(currentChar): - tokens.push(parseString()); - break; - - default: - throw new Error(`Unknown character: ${currentChar}`); - } - } - - return tokens; -} diff --git a/sandpack-client/src/clients/static/index.ts b/sandpack-client/src/clients/static/index.ts index 2d034c932..dbf962d02 100644 --- a/sandpack-client/src/clients/static/index.ts +++ b/sandpack-client/src/clients/static/index.ts @@ -1,5 +1,4 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import type { FilesMap } from "@codesandbox/nodebox"; + import type { FileContent } from "static-browser-server"; import { PreviewController } from "static-browser-server"; @@ -9,22 +8,23 @@ import type { SandboxSetup, UnsubscribeFunction, } from "../.."; -// get the bundled file, which contains all dependencies -// @ts-ignore +// @ts-expect-error // get the bundled file, which contains all dependencies import consoleHook from "../../inject-scripts/dist/consoleHook.js"; import { SandpackClient } from "../base"; import { EventEmitter } from "../event-emitter"; -import { fromBundlerFilesToFS, generateRandomId } from "../node/client.utils"; -import type { SandpackNodeMessage } from "../node/types"; +import { fromBundlerFilesToFS, generateRandomId } from "../vm/client.utils"; +import type { SandpackVMMessage } from "../vm/types"; import { insertHtmlAfterRegex, readBuffer, validateHtml } from "./utils"; +export type FilesMap = Record; + export class SandpackStatic extends SandpackClient { private emitter: EventEmitter; private previewController: PreviewController; private files: Map = new Map(); - public iframe!: HTMLIFrameElement; + public selector!: string; public element: Element; @@ -222,7 +222,7 @@ export class SandpackStatic extends SandpackClient { /** * Bundler communication */ - public dispatch(message: SandpackNodeMessage): void { + public dispatch(message: SandpackVMMessage): void { switch (message.type) { case "compile": this.compile(message.modules); diff --git a/sandpack-client/src/clients/static/utils.ts b/sandpack-client/src/clients/static/utils.ts index ac9639f02..1ce842ae6 100644 --- a/sandpack-client/src/clients/static/utils.ts +++ b/sandpack-client/src/clients/static/utils.ts @@ -4,7 +4,7 @@ export const insertHtmlAfterRegex = ( regex: RegExp, content: string, insertable: string -): string | void => { +) => { const match = regex.exec(content); if (match && match.length >= 1) { const offset = match.index + match[0].length; diff --git a/sandpack-client/src/clients/node/client.utils.test.ts b/sandpack-client/src/clients/vm/client.utils.test.ts similarity index 100% rename from sandpack-client/src/clients/node/client.utils.test.ts rename to sandpack-client/src/clients/vm/client.utils.test.ts diff --git a/sandpack-client/src/clients/vm/client.utils.ts b/sandpack-client/src/clients/vm/client.utils.ts new file mode 100644 index 000000000..d51a25667 --- /dev/null +++ b/sandpack-client/src/clients/vm/client.utils.ts @@ -0,0 +1,131 @@ +import type { SandboxSession } from "@codesandbox/sdk"; + +import { createError } from "../.."; +import type { SandpackBundlerFiles } from "../../"; + +let counter = 0; + +export function generateRandomId() { + const now = Date.now(); + const randomNumber = Math.round(Math.random() * 10000); + const count = (counter += 1); + return (+`${now}${randomNumber}${count}`).toString(16); +} + +export const writeBuffer = (content: string | Uint8Array): Uint8Array => { + if (typeof content === "string") { + return new TextEncoder().encode(content); + } else { + return content; + } +}; + +export const readBuffer = (content: string | Uint8Array): string => { + if (typeof content === "string") { + return content; + } else { + return new TextDecoder().decode(content); + } +}; + +export const fromBundlerFilesToFS = ( + files: SandpackBundlerFiles +): Record => { + return Object.entries(files).reduce>( + (acc, [key, value]) => { + acc[key] = writeBuffer(value.code); + + return acc; + }, + {} + ); +}; + +export const getMessageFromError = (error: Error | string): string => { + if (typeof error === "string") return error; + + if (typeof error === "object" && "message" in error) { + return error.message; + } + + return createError( + "The server could not be reached. Make sure that the node script is running and that a port has been started." + ); +}; + +export async function scanDirectory(dirPath: string, fs: SandboxSession["fs"]) { + const IGNORED_DIRS = new Set([ + "node_modules", + ".git", + "dist", + "build", + "coverage", + ".cache", + ".next", + ".nuxt", + ".output", + ".vscode", + ".idea", + ".devcontainer", + ".codesandbox", + "yarn.lock", + "pnpm-lock.yaml", + ]); + + const results: Array<{ path: string; content: Uint8Array }> = []; + + try { + const entries = await fs.readdir(dirPath); + + for (const entry of entries) { + const fullPath = dirPath + "/" + entry.name; + + if (entry.isSymlink || IGNORED_DIRS.has(entry.name)) { + continue; + } + + if (entry.type === "file") { + results.push({ + path: fullPath, + content: await fs.readFile(fullPath), + }); + } + + // Recursively scan subdirectories + if (entry.type === "directory") { + const subDirResults = await scanDirectory(fullPath, fs); + results.push(...subDirResults); + } + } + + return results; + } catch (error) { + console.error(`Error scanning directory ${dirPath}:`, error); + throw error; + } +} + +let groupId = 1; +export const createLogGroup = (group: string) => { + let logId = 1; + + // eslint-disable-next-line no-console + console.group(`[${groupId++}]: ${group}`); + + return { + // eslint-disable-next-line no-console + groupEnd: () => console.groupEnd(), + log: (...args: unknown[]): void => { + // eslint-disable-next-line no-console + console.debug(`[${logId++}]:`, ...args); + }, + }; +}; + +export const throwIfTimeout = (timeout: number) => { + return new Promise((_, reject) => + setTimeout(() => { + reject(new Error(`Timeout of ${timeout}ms exceeded`)); + }, timeout) + ); +}; diff --git a/sandpack-client/src/clients/node/iframe.utils.ts b/sandpack-client/src/clients/vm/iframe.utils.ts similarity index 100% rename from sandpack-client/src/clients/node/iframe.utils.ts rename to sandpack-client/src/clients/vm/iframe.utils.ts diff --git a/sandpack-client/src/clients/vm/index.ts b/sandpack-client/src/clients/vm/index.ts new file mode 100644 index 000000000..71578fc32 --- /dev/null +++ b/sandpack-client/src/clients/vm/index.ts @@ -0,0 +1,510 @@ +/* eslint-disable no-console,@typescript-eslint/no-explicit-any,prefer-rest-params,@typescript-eslint/explicit-module-boundary-types */ +import type { SandboxSession } from "@codesandbox/sdk"; +import type { PortInfo } from "@codesandbox/sdk"; +import { connectToSandbox } from "@codesandbox/sdk/browser"; + +import type { + ClientOptions, + ListenerFunction, + SandboxSetup, + UnsubscribeFunction, +} from "../.."; +import { nullthrows } from "../.."; +import { SandpackClient } from "../base"; +import { EventEmitter } from "../event-emitter"; + +import { + getMessageFromError, + readBuffer, + fromBundlerFilesToFS, + writeBuffer, + scanDirectory, + createLogGroup, + throwIfTimeout, +} from "./client.utils"; +import { loadPreviewIframe, setPreviewIframeProperties } from "./iframe.utils"; +import type { SandpackVMMessage } from "./types"; + +export type FileContent = Uint8Array | string; +export type FilesMap = Record; + +export class SandpackVM extends SandpackClient { + private emitter: EventEmitter; + private sandbox!: SandboxSession; + + private _modulesCache = new Map(); + private _forkPromise: Promise | null = null; + private _initPromise: Promise | null = null; + + constructor( + selector: string | HTMLIFrameElement, + sandboxInfo: SandboxSetup, + options: ClientOptions = {} + ) { + const initLog = createLogGroup("Setup"); + + super(selector, sandboxInfo, { + ...options, + bundlerURL: options.bundlerURL, + }); + + this.emitter = new EventEmitter(); + + // Assign iframes + this.manageIframes(selector); + initLog.log("Create iframe"); + + initLog.log("Trigger initial compile"); + initLog.groupEnd(); + // Trigger initial compile + this.updateSandbox(sandboxInfo); + } + + async ensureDirectoryExist(path: string): Promise { + if (path === ".") { + return Promise.resolve(); + } + + const directory = path.split("/").slice(0, -1).join("/"); + + if (directory === ".") { + return Promise.resolve(); + } + + try { + await this.sandbox.fs.mkdir(directory, true); + } catch { + // File already exists + } + } + + // Initialize sandbox, should only ever be called once + private async _init(files: FilesMap): Promise { + const initLog = createLogGroup("Initializing sandbox..."); + + this.dispatch({ + type: "vm/progress", + data: "[1/3] Fetching sandbox...", + }); + + initLog.log("Fetching sandbox..."); + + nullthrows( + this.options.vmEnvironmentApiUrl, + `No 'options.vmEnvironmentApiUrl' provided. This options is mandatory when using VM as environment` + ); + + nullthrows( + this.sandboxSetup.templateID, + `No templateID provided. This options is mandatory when using VM as environment` + ); + + const response = await fetch( + this.options.vmEnvironmentApiUrl!(this.sandboxSetup.templateID!) + ); + const sandpackData = await response.json(); + initLog.log("Fetching sandbox success", sandpackData); + + initLog.log("Connecting sandbox..."); + this.sandbox = await Promise.race([ + throwIfTimeout(15_000), + connectToSandbox(sandpackData), + ]); + initLog.log("Connecting sandbox success", this.sandbox); + initLog.groupEnd(); + + this.dispatch({ + type: "vm/progress", + data: "[2/3] Creating FS...", + }); + + const filesLog = createLogGroup("Files"); + filesLog.log("Writing files..."); + for (const [key, value] of Object.entries(files)) { + const path = key.startsWith(".") ? key : `.${key}`; + await this.ensureDirectoryExist(path); + + await this.sandbox.fs.writeFile(path, writeBuffer(value), { + create: true, + overwrite: true, + }); + } + filesLog.log("Writing files success"); + + filesLog.log("Scaning VM FS..."); + const vmFiles = await scanDirectory(".", this.sandbox.fs); + + vmFiles.forEach(({ path, content }) => { + const pathWithoutLeading = path.startsWith("./") + ? path.replace("./", "/") + : path; + + this._modulesCache.set(pathWithoutLeading, content); + + this.dispatch({ + type: "fs/change", + path: pathWithoutLeading, + content: readBuffer(content), + }); + }); + filesLog.log("Scaning VM FS success", vmFiles); + filesLog.groupEnd(); + + await this.globalListeners(); + } + + /** + * It initializes the emulator and provide it with files, template and script to run + */ + private async compile(files: FilesMap): Promise { + try { + this.status = "initializing"; + this.dispatch({ type: "start", firstLoad: true }); + if (!this._initPromise) { + this._initPromise = this._init(files); + } + await this._initPromise; + + this.dispatch({ type: "connected" }); + + await this.setLocationURLIntoIFrame(); + + this.dispatchDoneMessage(); + } catch (err) { + if (this.emitter.listenersCount === 0) { + throw err; + } + + this.dispatch({ + type: "action", + action: "notification", + notificationType: "error", + title: getMessageFromError(err as Error), + }); + + this.dispatch({ type: "done", compilatonError: true }); + } + } + + /** + * Nodebox needs to handle two types of iframes at the same time: + * + * 1. Runtime iframe: where the emulator process runs, which is responsible + * for creating the other iframes (hidden); + * 2. Preview iframes: any other node process that contains a PORT (public); + */ + private manageIframes(selector: string | HTMLIFrameElement): void { + /** + * Pick the preview iframe + */ + if (typeof selector === "string") { + const element = document.querySelector(selector); + + nullthrows(element, `The element '${selector}' was not found`); + + this.iframe = document.createElement("iframe"); + element?.appendChild(this.iframe); + } else { + this.iframe = selector; + } + + // Set preview iframe styles + setPreviewIframeProperties(this.iframe, this.options); + } + + private awaitForPorts(): Promise { + return new Promise((resolve) => { + const initPorts = this.sandbox.ports.getOpenedPorts(); + + if (initPorts.length > 0) { + resolve(initPorts); + + return; + } + + this.sandbox.ports.onDidPortOpen(() => { + resolve(this.sandbox.ports.getOpenedPorts()); + }); + }); + } + + private async setLocationURLIntoIFrame(): Promise { + const initLog = createLogGroup("Preview"); + + this.dispatch({ + type: "vm/progress", + data: "[3/3] Opening preview...", + }); + + initLog.log("Waiting for port..."); + const ports = await this.awaitForPorts(); + initLog.log("Ports found", ports); + + const mainPort = ports.sort((a, b) => { + return a.port - b.port; + })[0]; + + initLog.log("Defined main port", mainPort); + + initLog.log("Getting preview url for port..."); + const iframePreviewUrl = this.sandbox.ports.getPreviewUrl(mainPort.port); + initLog.log("Got preview url", iframePreviewUrl); + + if (iframePreviewUrl) { + initLog.log("Loading preview iframe..."); + await loadPreviewIframe(this.iframe, iframePreviewUrl); + initLog.log("Preview iframe loaded"); + } else { + initLog.log("No preview url found"); + } + + initLog.groupEnd(); + } + + /** + * Send all messages and events to tell to the + * consumer that the bundler is ready without any error + */ + private dispatchDoneMessage(): void { + this.status = "done"; + this.dispatch({ type: "done", compilatonError: false }); + } + + private async globalListeners(): Promise { + // TYPE: + // { + // "type": "change", + // "paths": [ + // "/project/sandbox/.git/FETCH_HEAD" + // ] + // } + // await this.sandbox.fs.watch( + // "./", + // { + // recursive: true, + // excludes: [ + // "**/node_modules/**", + // "**/build/**", + // "**/dist/**", + // "**/vendor/**", + // "**/.config/**", + // "**/.vuepress/**", + // "**/.git/**", + // "**/.next/**", + // "**/.nuxt/**", + // ], + // }, + // (message) => { + // console.log(message); + // } + // ); + // await this.sandbox.fs.watch( + // "*", + // { + // excludes: [ + // ".next", + // "node_modules", + // "build", + // "dist", + // "vendor", + // ".config", + // ".vuepress", + // ], + // }, + // async (message) => { + // if (!message) return; + // debugger; + // const event = message as FSWatchEvent; + // const path = + // "newPath" in event + // ? event.newPath + // : "path" in event + // ? event.path + // : ""; + // const { type } = await this.sandbox.fs.stat(path); + // if (type !== "file") return null; + // try { + // switch (event.type) { + // case "change": + // case "create": { + // const content = await this.sandbox.fs.readFile(event.path); + // this.dispatch({ + // type: "fs/change", + // path: event.path, + // content: readBuffer(content), + // }); + // this._modulesCache.set(event.path, writeBuffer(content)); + // break; + // } + // case "remove": + // this.dispatch({ + // type: "fs/remove", + // path: event.path, + // }); + // this._modulesCache.delete(event.path); + // break; + // case "rename": { + // this.dispatch({ + // type: "fs/remove", + // path: event.oldPath, + // }); + // this._modulesCache.delete(event.oldPath); + // const newContent = await this.sandbox.fs.readFile(event.newPath); + // this.dispatch({ + // type: "fs/change", + // path: event.newPath, + // content: readBuffer(newContent), + // }); + // this._modulesCache.set(event.newPath, writeBuffer(newContent)); + // break; + // } + // case "close": + // break; + // } + // } catch (err) { + // this.dispatch({ + // type: "action", + // action: "notification", + // notificationType: "error", + // title: getMessageFromError(err as Error), + // }); + // } + // } + // ); + } + + public async fork() { + this.dispatch({ + type: "vm/progress", + data: "Forking sandbox...", + }); + + const timer = setTimeout(() => { + this.dispatch({ + type: "vm/progress", + data: "Still forking...", + }); + }, 3_000); + + const response = await fetch( + this.options.vmEnvironmentApiUrl!(this.sandbox.id), + { method: "POST" } + ); + const sandpackData = await response.json(); + this.sandbox = await Promise.race([ + throwIfTimeout(10_000), + connectToSandbox(sandpackData), + ]); + + clearTimeout(timer); + + this.dispatch({ + type: "vm/progress", + data: "Assigining new preview...", + }); + this.setLocationURLIntoIFrame(); + + this.dispatch({ + type: "done", + compilatonError: false, + }); + } + + /** + * PUBLIC Methods + */ + public async updateSandbox(setup: SandboxSetup) { + const modules = fromBundlerFilesToFS(setup.files); + + /** + * Update file changes + */ + if (this.status === "done") { + // Stack pending requests + await this._forkPromise; + + const needToFork = this.sandboxSetup.templateID === this.sandbox.id; + if (needToFork) { + this._forkPromise = this.fork(); + await this._forkPromise; + } + + for await (const [key, value] of Object.entries(modules)) { + if ( + !this._modulesCache.get(key) || + readBuffer(value) !== readBuffer(this._modulesCache.get(key)) + ) { + const ensureLeadingPath = key.startsWith(".") ? key : "." + key; + await this.ensureDirectoryExist(ensureLeadingPath); + console.log(this._modulesCache, key); + try { + this.sandbox.fs.writeFile(ensureLeadingPath, writeBuffer(value), { + create: true, + overwrite: true, + }); + } catch (error) { + console.error(error); + } + } + } + + return; + } + + /** + * Pass init files to the bundler + */ + this.dispatch({ + codesandbox: true, + modules, + template: setup.template, + type: "compile", + }); + + /** + * Add modules to cache, this will ensure uniqueness changes + * + * Keep it after the compile action, in order to update the cache at the right moment + */ + Object.entries(modules).forEach(([key, value]) => { + this._modulesCache.set(key, writeBuffer(value)); + }); + } + + public async dispatch(message: SandpackVMMessage): Promise { + switch (message.type) { + case "compile": + this.compile(message.modules); + break; + + case "refresh": + await this.setLocationURLIntoIFrame(); + break; + + case "urlback": + case "urlforward": + this.iframe?.contentWindow?.postMessage(message, "*"); + break; + + case "vm/request_editor_url": { + this.dispatch({ + type: "vm/response_editor_url", + data: `https://codesandbox.io/p/redirect-to-project-editor/${this.sandbox.id}`, + }); + + break; + } + + default: + this.emitter.dispatch(message); + } + } + + public listen(listener: ListenerFunction): UnsubscribeFunction { + return this.emitter.listener(listener); + } + + public destroy(): void { + this.emitter.cleanup(); + } +} diff --git a/sandpack-client/src/clients/node/inject-scripts/historyListener.ts b/sandpack-client/src/clients/vm/inject-scripts/historyListener.ts similarity index 100% rename from sandpack-client/src/clients/node/inject-scripts/historyListener.ts rename to sandpack-client/src/clients/vm/inject-scripts/historyListener.ts diff --git a/sandpack-client/src/clients/node/inject-scripts/index.ts b/sandpack-client/src/clients/vm/inject-scripts/index.ts similarity index 76% rename from sandpack-client/src/clients/node/inject-scripts/index.ts rename to sandpack-client/src/clients/vm/inject-scripts/index.ts index 86bb62cc5..624dda4b7 100644 --- a/sandpack-client/src/clients/node/inject-scripts/index.ts +++ b/sandpack-client/src/clients/vm/inject-scripts/index.ts @@ -1,8 +1,5 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import type { InjectMessage } from "@codesandbox/nodebox"; -import { INJECT_MESSAGE_TYPE } from "@codesandbox/nodebox"; - // get the bundled file, which contains all dependencies // @ts-ignore import consoleHook from "../../../inject-scripts/dist/consoleHook.js"; @@ -10,6 +7,19 @@ import consoleHook from "../../../inject-scripts/dist/consoleHook.js"; import { setupHistoryListeners } from "./historyListener"; import { watchResize } from "./resize.js"; +const INJECT_MESSAGE_TYPE = "INJECT_AND_INVOKE"; + +export interface Message { + type: string; +} +type BaseScope = Record; +export interface InjectMessage { + uid: string; + type: typeof INJECT_MESSAGE_TYPE; + code: string; + scope: Scope; +} + const scripts = [ { code: setupHistoryListeners.toString(), id: "historyListener" }, { diff --git a/sandpack-client/src/clients/node/inject-scripts/resize.ts b/sandpack-client/src/clients/vm/inject-scripts/resize.ts similarity index 100% rename from sandpack-client/src/clients/node/inject-scripts/resize.ts rename to sandpack-client/src/clients/vm/inject-scripts/resize.ts diff --git a/sandpack-client/src/clients/node/types.ts b/sandpack-client/src/clients/vm/types.ts similarity index 79% rename from sandpack-client/src/clients/node/types.ts rename to sandpack-client/src/clients/vm/types.ts index de519a0b0..d5ab3322d 100644 --- a/sandpack-client/src/clients/node/types.ts +++ b/sandpack-client/src/clients/vm/types.ts @@ -1,4 +1,4 @@ -import type { FilesMap, WorkerStatusUpdate } from "@codesandbox/nodebox"; +import type { FilesMap } from "@codesandbox/nodebox"; import type { BaseSandpackMessage, @@ -55,22 +55,19 @@ type SandpackURLsMessages = type: "urlforward"; }; -type SandpackShellMessages = - | { type: "shell/restart" } - | { type: "shell/openPreview" } - | { type: "shell/progress"; data: WorkerStatusUpdate & { command?: string } }; - -export type SandpackNodeMessage = BaseSandpackMessage & +export type SandpackVMMessage = BaseSandpackMessage & ( | SandpackStandartMessages | SandpackURLsMessages | SandpackBundlerMessages - | SandpackShellMessages | { type: "connected" } | { type: "stdout"; payload: SandpackShellStdoutData; } + | { type: "vm/progress"; data: string } + | { type: "vm/request_editor_url" } + | { type: "vm/response_editor_url"; data: string } | SandpackFSMessages ); diff --git a/sandpack-client/src/types.ts b/sandpack-client/src/types.ts index 9e4fc8ce8..47f229269 100644 --- a/sandpack-client/src/types.ts +++ b/sandpack-client/src/types.ts @@ -1,5 +1,5 @@ -import type { SandpackNodeMessage } from "./clients/node/types"; import type { SandpackRuntimeMessage } from "./clients/runtime/types"; +import type { SandpackVMMessage } from "./clients/vm/types"; export interface ClientOptions { /** @@ -76,6 +76,11 @@ export interface ClientOptions { */ experimental_enableServiceWorker?: boolean; experimental_stableServiceWorkerId?: string; + + /** + * URL Api to connect to the server endpoint when using VM environment + */ + vmEnvironmentApiUrl?: (id: string) => string; } export interface SandboxSetup { @@ -83,6 +88,7 @@ export interface SandboxSetup { dependencies?: Dependencies; devDependencies?: Dependencies; entry?: string; + templateID?: string; /** * What template we use, if not defined we infer the template from the dependencies or files. * @@ -176,7 +182,7 @@ export interface BundlerState { transpiledModules: Record; } -export type SandpackMessage = SandpackRuntimeMessage | SandpackNodeMessage; +export type SandpackMessage = SandpackRuntimeMessage | SandpackVMMessage; export type ListenerFunction = (msg: SandpackMessage) => void; export type UnsubscribeFunction = () => void; @@ -397,4 +403,5 @@ export type SandpackTemplate = | "static" | "solid" | "nextjs" - | "node"; + | "node" + | "vm"; diff --git a/sandpack-client/src/utils.ts b/sandpack-client/src/utils.ts index d2d8eba47..5b0e71bea 100644 --- a/sandpack-client/src/utils.ts +++ b/sandpack-client/src/utils.ts @@ -52,7 +52,6 @@ export function addPackageJSONIfNeeded( */ if (!packageJsonFile) { nullthrows(dependencies, DEPENDENCY_ERROR_MESSAGE); - nullthrows(entry, ENTRY_ERROR_MESSAGE); normalizedFilesPath["/package.json"] = { code: createPackageJSON(dependencies, devDependencies, entry), diff --git a/sandpack-client/tsconfig.json b/sandpack-client/tsconfig.json index a6d8bc12e..d556cd19e 100644 --- a/sandpack-client/tsconfig.json +++ b/sandpack-client/tsconfig.json @@ -2,6 +2,8 @@ "compilerOptions": { "lib": ["es2015", "es2016", "es2017", "ES2019", "dom"], "strict": true, + "module": "ESNext", + "moduleResolution": "bundler", "sourceMap": false, "emitDeclarationOnly": true, "declaration": true, @@ -10,7 +12,6 @@ "emitDecoratorMetadata": true, "noImplicitAny": true, "typeRoots": ["node_modules/@types"], - "outDir": "dist", "skipLibCheck": true }, "include": ["./src"], diff --git a/sandpack-environments/.bundler b/sandpack-environments/.bundler new file mode 100644 index 000000000..a018049f8 --- /dev/null +++ b/sandpack-environments/.bundler @@ -0,0 +1,3 @@ +// Manually generated file to trigger new releases based on the bundler changes. +// The following value is the commit hash from codesandbox-client +f64b210d3832ce8558516bcda9bebd543400c9d9 diff --git a/sandpack-environments/.eslintignore b/sandpack-environments/.eslintignore new file mode 100644 index 000000000..5971abb9a --- /dev/null +++ b/sandpack-environments/.eslintignore @@ -0,0 +1,5 @@ +dist/ +esm/ +sandpack/ +file-resolver-protocol.ts +examples/ \ No newline at end of file diff --git a/sandpack-environments/.gitignore b/sandpack-environments/.gitignore new file mode 100644 index 000000000..14537d2eb --- /dev/null +++ b/sandpack-environments/.gitignore @@ -0,0 +1,7 @@ +compiled +storybook-static +.rpt2_cache +dist +esm +docs +sandpack/ diff --git a/sandpack-environments/CHANGELOG.md b/sandpack-environments/CHANGELOG.md new file mode 100644 index 000000000..1dd22e3f6 --- /dev/null +++ b/sandpack-environments/CHANGELOG.md @@ -0,0 +1,696 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.19.8](https://github.com/codesandbox/sandpack/compare/v2.19.7...v2.19.8) (2024-09-12) + +### Bug Fixes + +- **sandpack-id:** generate new bundler id based on client version ([#1202](https://github.com/codesandbox/sandpack/issues/1202)) ([dbb882e](https://github.com/codesandbox/sandpack/commit/dbb882eec6963a11c2eac7293c0891b501f9a9fe)) + +## [2.19.7](https://github.com/codesandbox/sandpack/compare/v2.19.6...v2.19.7) (2024-09-11) + +### Bug Fixes + +- **client:** postcss transpile to angular template ([#1201](https://github.com/codesandbox/sandpack/issues/1201)) ([0b57de6](https://github.com/codesandbox/sandpack/commit/0b57de653d3bea38917c2665aae3942c87a212ff)) + +## [2.19.4](https://github.com/codesandbox/sandpack/compare/v2.19.3...v2.19.4) (2024-09-10) + +### Bug Fixes + +- **sw:** get transpiled files from bundler ([#1196](https://github.com/codesandbox/sandpack/issues/1196)) ([4563646](https://github.com/codesandbox/sandpack/commit/4563646a08d3b072dec14c0942af46f9755d2719)) + +# [2.19.0](https://github.com/codesandbox/sandpack/compare/v2.18.3...v2.19.0) (2024-08-22) + +### Features + +- **iframe allow:** introduce xr-spatial-tracking ([#1183](https://github.com/codesandbox/sandpack/issues/1183)) ([7245731](https://github.com/codesandbox/sandpack/commit/72457311c01c9d6ece540e177007e59b42a64153)) + +## [2.18.2](https://github.com/codesandbox/sandpack/compare/v2.18.1...v2.18.2) (2024-07-31) + +### Bug Fixes + +- **client:** use stable sw id ([#1169](https://github.com/codesandbox/sandpack/issues/1169)) ([8c15b85](https://github.com/codesandbox/sandpack/commit/8c15b85018fae9d55bfad86922c736694e45848f)) + +## [2.18.1](https://github.com/codesandbox/sandpack/compare/v2.18.0...v2.18.1) (2024-07-25) + +### Bug Fixes + +- **sw:** assign new channel port on reload ([#1166](https://github.com/codesandbox/sandpack/issues/1166)) ([2d92bea](https://github.com/codesandbox/sandpack/commit/2d92bea4a9754373027455494050110d91bd13fc)) + +# [2.18.0](https://github.com/codesandbox/sandpack/compare/v2.17.1...v2.18.0) (2024-07-11) + +### Features + +- Add support for enabling service worker feature ([#1127](https://github.com/codesandbox/sandpack/issues/1127)) ([4e2b116](https://github.com/codesandbox/sandpack/commit/4e2b1167446be930e4b072f785f719aa7444b5a9)) + +## [2.17.1](https://github.com/codesandbox/sandpack/compare/v2.17.0...v2.17.1) (2024-07-11) + +### Bug Fixes + +- **client:** update bundler ([#1160](https://github.com/codesandbox/sandpack/issues/1160)) ([a0c4209](https://github.com/codesandbox/sandpack/commit/a0c4209ce1172b2a9cdadd21dc3007d7f7cbfb1f)) + +# [2.17.0](https://github.com/codesandbox/sandpack/compare/v2.16.1...v2.17.0) (2024-07-09) + +### Features + +- **client:** enable SW in the bundler ([#1159](https://github.com/codesandbox/sandpack/issues/1159)) ([36e580b](https://github.com/codesandbox/sandpack/commit/36e580b5b76ee34bb722027b35302fa6c75e521e)) + +## [2.16.1](https://github.com/codesandbox/sandpack/compare/v2.16.0...v2.16.1) (2024-07-08) + +### Bug Fixes + +- **loading:** update bundler to consume the correct loading message ([#1157](https://github.com/codesandbox/sandpack/issues/1157)) ([7a70999](https://github.com/codesandbox/sandpack/commit/7a7099991e8831a5ca76b56cada5ec06b17aa339)) + +# [2.16.0](https://github.com/codesandbox/sandpack/compare/v2.15.0...v2.16.0) (2024-07-08) + +### Features + +- **loading:** show dependency download progress ([#1146](https://github.com/codesandbox/sandpack/issues/1146)) ([a811267](https://github.com/codesandbox/sandpack/commit/a811267243785bb461fafed3228a90e8630d022f)) + +## [2.14.5](https://github.com/codesandbox/sandpack/compare/v2.14.4...v2.14.5) (2024-07-03) + +### Bug Fixes + +- **bundler:** use latest bundler version ([#1155](https://github.com/codesandbox/sandpack/issues/1155)) ([260cb35](https://github.com/codesandbox/sandpack/commit/260cb35be7a669cf337533a0c1b970f04bc8571c)) + +## [2.14.4](https://github.com/codesandbox/sandpack/compare/v2.14.3...v2.14.4) (2024-06-18) + +### Bug Fixes + +- **client:** bump version ([#1151](https://github.com/codesandbox/sandpack/issues/1151)) ([a2e56c0](https://github.com/codesandbox/sandpack/commit/a2e56c0bcdacb242295fb545ab8d08231adfba21)) + +## [2.14.3](https://github.com/codesandbox/sandpack/compare/v2.14.2...v2.14.3) (2024-06-18) + +### Bug Fixes + +- **preview:** allows clipboard api ([#1149](https://github.com/codesandbox/sandpack/issues/1149)) ([7350364](https://github.com/codesandbox/sandpack/commit/7350364b3dbf933ac04686be284222994335c001)) + +## [2.13.8](https://github.com/codesandbox/sandpack/compare/v2.13.7...v2.13.8) (2024-04-11) + +### Bug Fixes + +- force new release ([#1118](https://github.com/codesandbox/sandpack/issues/1118)) ([5b38b37](https://github.com/codesandbox/sandpack/commit/5b38b372de2d9ff26967fec1e22c772c755a0d33)) + +## [2.13.7](https://github.com/codesandbox/sandpack/compare/v2.13.6...v2.13.7) (2024-03-26) + +### Bug Fixes + +- Fixed sandpackNode initializing failure ([#1105](https://github.com/codesandbox/sandpack/issues/1105)) ([26685d9](https://github.com/codesandbox/sandpack/commit/26685d92973ab609bae03dadca4d7f21c1fd696c)) + +## [2.13.6](https://github.com/codesandbox/sandpack/compare/v2.13.5...v2.13.6) (2024-03-25) + +### Bug Fixes + +- **pro:** use partitioned cookie for sandpack authentication ([#1110](https://github.com/codesandbox/sandpack/issues/1110)) ([2950186](https://github.com/codesandbox/sandpack/commit/29501863c2ebbcbf64fb9e7080feed1f2bc724b5)) + +## [2.13.2](https://github.com/codesandbox/sandpack/compare/v2.13.1...v2.13.2) (2024-02-24) + +### Bug Fixes + +- **compile opts:** don't overwrite default properties ([#1090](https://github.com/codesandbox/sandpack/issues/1090)) ([2877fcf](https://github.com/codesandbox/sandpack/commit/2877fcf46be7579a20d793b5ebb746e63622fb74)) + +# [2.13.0](https://github.com/codesandbox/sandpack/compare/v2.12.1...v2.13.0) (2024-02-22) + +### Features + +- Add spread operator for options in runtime client ([#1086](https://github.com/codesandbox/sandpack/issues/1086)) ([b7c7551](https://github.com/codesandbox/sandpack/commit/b7c7551472e42723a70db7cbf7af853810dfd9d3)) + +# [2.12.0](https://github.com/codesandbox/sandpack/compare/v2.11.3...v2.12.0) (2024-02-05) + +### Features + +- sandpack template type ([#1075](https://github.com/codesandbox/sandpack/issues/1075)) ([db8eba7](https://github.com/codesandbox/sandpack/commit/db8eba7d7810896948e29067ac6606388e31c5e2)) + +## [2.11.2](https://github.com/codesandbox/sandpack/compare/v2.11.1...v2.11.2) (2024-01-11) + +### Bug Fixes + +- bump codesandbox-client and test new publish script ([#1056](https://github.com/codesandbox/sandpack/issues/1056)) ([1736185](https://github.com/codesandbox/sandpack/commit/173618538584f11426c7b2156243bc1691303696)) + +## [2.11.1](https://github.com/codesandbox/sandpack/compare/v2.11.0...v2.11.1) (2024-01-10) + +### Bug Fixes + +- add `allow-downloads` to iframes ([#1054](https://github.com/codesandbox/sandpack/issues/1054)) ([c038e13](https://github.com/codesandbox/sandpack/commit/c038e1322bbca94ea529f7a92089ad0f605d1ba5)) + +# [2.10.0](https://github.com/codesandbox/sandpack/compare/v2.9.0...v2.10.0) (2023-11-15) + +### Features + +- **node:** Added resize event ([#1037](https://github.com/codesandbox/sandpack/issues/1037)) ([61ccf7e](https://github.com/codesandbox/sandpack/commit/61ccf7ebfb954710fed40d26f800d8e145806006)) +- Upgrade Node.js version ([#1029](https://github.com/codesandbox/sandpack/issues/1029)) ([a79a5d2](https://github.com/codesandbox/sandpack/commit/a79a5d2feca6800e1d967df688a555aa39f8c5e6)) + +# [2.9.0](https://github.com/codesandbox/sandpack/compare/v2.8.0...v2.9.0) (2023-10-06) + +### Features + +- add sandbox-id prop to compile ([#1015](https://github.com/codesandbox/sandpack/issues/1015)) ([600b984](https://github.com/codesandbox/sandpack/commit/600b984d4dccaefc36e87d41a56bbd4cb9bb434f)) + +## [2.7.1](https://github.com/codesandbox/sandpack/compare/v2.7.0...v2.7.1) (2023-09-13) + +### Bug Fixes + +- **node:** fix issue with undefined startRoute in iframePreviewUrl ([#1002](https://github.com/codesandbox/sandpack/issues/1002)) ([c457a53](https://github.com/codesandbox/sandpack/commit/c457a5330d4c0eb9bf4bf35ab4af7c9171a5680e)) + +# [2.7.0](https://github.com/codesandbox/sandpack/compare/v2.6.9...v2.7.0) (2023-09-11) + +### Features + +- add console hook to static template ([#909](https://github.com/codesandbox/sandpack/issues/909)) ([1a473e3](https://github.com/codesandbox/sandpack/commit/1a473e3fa2a4d6581d8a1c4e30586588f5a9ee9b)) + +## [2.6.9](https://github.com/codesandbox/sandpack/compare/v2.6.8...v2.6.9) (2023-06-20) + +### Bug Fixes + +- **lerna:** remove experimental flag for workspaces ([#956](https://github.com/codesandbox/sandpack/issues/956)) ([dec34f0](https://github.com/codesandbox/sandpack/commit/dec34f02bcbb92b0f3d62c305d58cd27a7db6b3b)) + +## [2.6.8](https://github.com/codesandbox/sandpack/compare/v2.6.7...v2.6.8) (2023-06-20) + +### Bug Fixes + +- **client:** hard reload on module removal ([d5dfdd1](https://github.com/codesandbox/sandpack/commit/d5dfdd126d1061d63e1f1b086f30cbe7077ca30b)) + +## [2.6.7](https://github.com/codesandbox/sandpack/compare/v2.6.6...v2.6.7) (2023-05-26) + +### Bug Fixes + +- **client:** avoid concurrent compile step and init ([#946](https://github.com/codesandbox/sandpack/issues/946)) ([98a20e9](https://github.com/codesandbox/sandpack/commit/98a20e92ebb3aecb7ef23f18a5d3cb76f716b0a4)) +- **node:** remove --force option from all Sandpack Vite templates ([#947](https://github.com/codesandbox/sandpack/issues/947)) ([4d1b576](https://github.com/codesandbox/sandpack/commit/4d1b576adaa607e460b17a44f6d2b4c2349a2648)) + +## [2.6.6](https://github.com/codesandbox/sandpack/compare/v2.6.5...v2.6.6) (2023-05-22) + +### Bug Fixes + +- **compile:** create one TestRunner between sandbox compiles ([5ada4e3](https://github.com/codesandbox/sandpack/commit/5ada4e3e80509a0810e39baf8a638675991bb7dd)) + +## [2.6.5](https://github.com/codesandbox/sandpack/compare/v2.6.4...v2.6.5) (2023-05-19) + +### Bug Fixes + +- **package.json:** update @codesandbox/nodebox and outvariant versions ([#942](https://github.com/codesandbox/sandpack/issues/942)) ([e20d475](https://github.com/codesandbox/sandpack/commit/e20d475fdd5bd63c6597e4ba9367d7f9c49aa3ff)) + +## [2.6.4](https://github.com/codesandbox/sandpack/compare/v2.6.3...v2.6.4) (2023-05-11) + +### Bug Fixes + +- **runtime-client:** disable loading state by default ([#935](https://github.com/codesandbox/sandpack/issues/935)) ([aee0e3d](https://github.com/codesandbox/sandpack/commit/aee0e3dd76548c43c7439ce353200685ab7f5288)) + +## [2.6.3](https://github.com/codesandbox/sandpack/compare/v2.6.2...v2.6.3) (2023-05-05) + +### Bug Fixes + +- **client:** work around solid refresh bugs ([b11b5c7](https://github.com/codesandbox/sandpack/commit/b11b5c79d1bfbbf0806e68a9ae74eeebb23c58f1)) + +## [2.6.2](https://github.com/codesandbox/sandpack/compare/v2.6.1...v2.6.2) (2023-05-02) + +### Bug Fixes + +- **useClient:** track all clients and update clients on runSandpack ([#923](https://github.com/codesandbox/sandpack/issues/923)) ([a7334ad](https://github.com/codesandbox/sandpack/commit/a7334adfb712abd2342c3fa949f72f201c7ba2c4)) + +# [2.6.0](https://github.com/codesandbox/sandpack/compare/v2.5.0...v2.6.0) (2023-04-11) + +### Features + +- private dep v2 ([#746](https://github.com/codesandbox/sandpack/issues/746)) ([4fef453](https://github.com/codesandbox/sandpack/commit/4fef453b78444d41a4917629545decc091ff3cb6)) + +# [2.5.0](https://github.com/codesandbox/sandpack/compare/v2.4.11...v2.5.0) (2023-04-11) + +### Features + +- **task-manager:** parse commands ([#892](https://github.com/codesandbox/sandpack/issues/892)) ([7b5f25c](https://github.com/codesandbox/sandpack/commit/7b5f25c1355ab290f67b253e9de845825cc8ddb2)) + +## [2.4.9](https://github.com/codesandbox/sandpack/compare/v2.4.8...v2.4.9) (2023-04-10) + +### Bug Fixes + +- **Static Template:** Ensure valid HTML formatting for static template ([#894](https://github.com/codesandbox/sandpack/issues/894)) ([af68579](https://github.com/codesandbox/sandpack/commit/af68579a8cd2177608110d6a5c87006c31d77cd6)) + +## [2.4.7](https://github.com/codesandbox/sandpack/compare/v2.4.6...v2.4.7) (2023-04-08) + +### Bug Fixes + +- **Static Template:** fix doctype injection ([#899](https://github.com/codesandbox/sandpack/issues/899)) ([9d4a1c7](https://github.com/codesandbox/sandpack/commit/9d4a1c70d2353946adc78fea0bd3b900322e7888)) + +## [2.4.5](https://github.com/codesandbox/sandpack/compare/v2.4.4...v2.4.5) (2023-04-07) + +### Bug Fixes + +- **client:** remove buffer dependency ([#891](https://github.com/codesandbox/sandpack/issues/891)) ([307e52b](https://github.com/codesandbox/sandpack/commit/307e52b3fb59fcef4942c27a14dd51326ebbe649)) + +## [2.4.3](https://github.com/codesandbox/sandpack/compare/v2.4.2...v2.4.3) (2023-04-07) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [2.4.0](https://github.com/codesandbox/sandpack/compare/v2.3.2...v2.4.0) (2023-04-06) + +### Features + +- **Static Template:** add hidden head tags option ([#884](https://github.com/codesandbox/sandpack/issues/884)) ([3cee76f](https://github.com/codesandbox/sandpack/commit/3cee76fdd937460e379ddffea52f462c34ed5d36)) + +## [2.3.1](https://github.com/codesandbox/sandpack/compare/v2.3.0...v2.3.1) (2023-04-06) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [2.3.0](https://github.com/codesandbox/sandpack/compare/v2.2.9...v2.3.0) (2023-04-05) + +### Features + +- **Preview:** add startRoute prop to override Provider default ([#868](https://github.com/codesandbox/sandpack/issues/868)) ([bc28871](https://github.com/codesandbox/sandpack/commit/bc288719afd057d8699cf10de13f905bf1dcded9)) + +## [2.2.9](https://github.com/codesandbox/sandpack/compare/v2.2.8...v2.2.9) (2023-04-03) + +### Bug Fixes + +- **static:** Don't crash at inserting runtime ([#878](https://github.com/codesandbox/sandpack/issues/878)) ([7da3346](https://github.com/codesandbox/sandpack/commit/7da3346aa0f79abc24c4ecf51fd7506bbf590dd7)) + +## [2.2.6](https://github.com/codesandbox/sandpack/compare/v2.2.5...v2.2.6) (2023-04-03) + +### Bug Fixes + +- Don't add index logic to StaticSandbox ([#874](https://github.com/codesandbox/sandpack/issues/874)) ([146287b](https://github.com/codesandbox/sandpack/commit/146287b6fc09c6cf6335c1167f1425a3ec636614)) + +## [2.2.4](https://github.com/codesandbox/sandpack/compare/v2.2.3...v2.2.4) (2023-04-01) + +### Bug Fixes + +- **SandpackConsole:** make showHeader flag works ([#867](https://github.com/codesandbox/sandpack/issues/867)) ([54fd641](https://github.com/codesandbox/sandpack/commit/54fd64181e5318c396815ecd58baa1e2316d75a8)) + +## [2.2.1](https://github.com/codesandbox/sandpack/compare/v2.2.0...v2.2.1) (2023-04-01) + +### Bug Fixes + +- **clients:** allow clipboard write ([#864](https://github.com/codesandbox/sandpack/issues/864)) ([7ec95e1](https://github.com/codesandbox/sandpack/commit/7ec95e1dfdbf1d943a6ef063f75dd14650bbba1a)) + +# [2.2.0](https://github.com/codesandbox/sandpack/compare/v2.1.11...v2.2.0) (2023-03-31) + +### Features + +- static template ([#830](https://github.com/codesandbox/sandpack/issues/830)) ([2b14ed2](https://github.com/codesandbox/sandpack/commit/2b14ed226c7fdfe49054c6efe732f7f9f560b23c)) + +## [2.1.9](https://github.com/codesandbox/sandpack/compare/v2.1.8...v2.1.9) (2023-03-17) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [2.1.0](https://github.com/codesandbox/sandpack/compare/v2.0.29...v2.1.0) (2023-03-06) + +### Features + +- Improved loading and restarts ([#805](https://github.com/codesandbox/sandpack/issues/805)) ([1e1dffb](https://github.com/codesandbox/sandpack/commit/1e1dffb451f36b56084a87497e0da77ec6f16e29)) + +## [2.0.29](https://github.com/codesandbox/sandpack/compare/v2.0.28...v2.0.29) (2023-03-05) + +### Reverts + +- **remove outvariant:** remove it as it doesn't support esm ([#801](https://github.com/codesandbox/sandpack/issues/801)) ([dd47b0d](https://github.com/codesandbox/sandpack/commit/dd47b0d1e7811ab72d7ea48ee5c5256a79a69731)) + +## [2.0.28](https://github.com/codesandbox/sandpack/compare/v2.0.27...v2.0.28) (2023-03-05) + +### Bug Fixes + +- **remove outvariant:** remove it as it doesn't support esm ([#800](https://github.com/codesandbox/sandpack/issues/800)) ([3c1faef](https://github.com/codesandbox/sandpack/commit/3c1faefb5d2989c6ba75357ea743dbeb3cf71e5d)) + +## [2.0.26](https://github.com/codesandbox/sandpack/compare/v2.0.25...v2.0.26) (2023-03-02) + +### Bug Fixes + +- **preview:** add stdout for long process ([#792](https://github.com/codesandbox/sandpack/issues/792)) ([4da4d99](https://github.com/codesandbox/sandpack/commit/4da4d997b54beb408d3ec2f15d027090d05879c7)) + +## [2.0.25](https://github.com/codesandbox/sandpack/compare/v2.0.24...v2.0.25) (2023-03-02) + +### Bug Fixes + +- throw error on timeout ([#791](https://github.com/codesandbox/sandpack/issues/791)) ([3c201aa](https://github.com/codesandbox/sandpack/commit/3c201aa1edc0bd16ad7045bd8c6303f7fdeba289)) + +## [2.0.24](https://github.com/codesandbox/sandpack/compare/v2.0.23...v2.0.24) (2023-03-02) + +### Bug Fixes + +- increase preview timeout ([#789](https://github.com/codesandbox/sandpack/issues/789)) ([27fe67b](https://github.com/codesandbox/sandpack/commit/27fe67b986b81eee31f62c134fb08e47e002f7ee)) + +## [2.0.23](https://github.com/codesandbox/sandpack/compare/v2.0.22...v2.0.23) (2023-03-01) + +### Bug Fixes + +- **nodebox:** writeFile recursively ([#783](https://github.com/codesandbox/sandpack/issues/783)) ([9e61ef0](https://github.com/codesandbox/sandpack/commit/9e61ef0518677f2742d9b372a773af133ca4a2e5)) + +## [2.0.21](https://github.com/codesandbox/sandpack/compare/v2.0.20...v2.0.21) (2023-02-28) + +### Bug Fixes + +- **nodebox:** consider new files from Sandpack ([#778](https://github.com/codesandbox/sandpack/issues/778)) ([877222e](https://github.com/codesandbox/sandpack/commit/877222ef649ed741534709c834053eeefce70948)) + +## [2.0.20](https://github.com/codesandbox/sandpack/compare/v2.0.19...v2.0.20) (2023-02-28) + +### Bug Fixes + +- **sandpack-client:** setup build with rollup ([#758](https://github.com/codesandbox/sandpack/issues/758)) ([f645119](https://github.com/codesandbox/sandpack/commit/f6451194a718a0679ce5fcb4d64d1f0d58f6c146)) + +## [2.0.17](https://github.com/codesandbox/sandpack/compare/v2.0.16...v2.0.17) (2023-02-24) + +### Bug Fixes + +- **use-files:** make files reference more stable ([#760](https://github.com/codesandbox/sandpack/issues/760)) ([32ab419](https://github.com/codesandbox/sandpack/commit/32ab419bdd082353408ca2cfb7e2dba6d52caf08)) + +## [2.0.15](https://github.com/codesandbox/sandpack/compare/v2.0.14...v2.0.15) (2023-02-22) + +### Bug Fixes + +- Send serialized console output to parent ([#757](https://github.com/codesandbox/sandpack/issues/757)) ([5865fc5](https://github.com/codesandbox/sandpack/commit/5865fc51ae18a877194bd6832df12d0de86b38e0)) + +## [2.0.13](https://github.com/codesandbox/sandpack/compare/v2.0.12...v2.0.13) (2023-02-22) + +### Bug Fixes + +- **nodebox:** support env vars on commands ([#755](https://github.com/codesandbox/sandpack/issues/755)) ([af8d5c1](https://github.com/codesandbox/sandpack/commit/af8d5c1cfa0d9ef525565ec62c77459d745dfbf2)) + +## [2.0.11](https://github.com/codesandbox/sandpack/compare/v2.0.10...v2.0.11) (2023-02-22) + +### Bug Fixes + +- **sandpack-client:** move console-feed to dev deps ([#747](https://github.com/codesandbox/sandpack/issues/747)) ([bdd1bb7](https://github.com/codesandbox/sandpack/commit/bdd1bb7925f2511aaea594593e3f7f3b7b9c3613)) + +## [2.0.10](https://github.com/codesandbox/sandpack/compare/v2.0.9...v2.0.10) (2023-02-21) + +### Bug Fixes + +- **client:** enable code-splitting and dynamic imports in esbuild ([#741](https://github.com/codesandbox/sandpack/issues/741)) ([0bc9d80](https://github.com/codesandbox/sandpack/commit/0bc9d80f05d2c1bd35f2b74e1a4de96d9ddd4839)) + +## [2.0.8](https://github.com/codesandbox/sandpack/compare/v2.0.7...v2.0.8) (2023-02-20) + +### Bug Fixes + +- **sandpack-client:** fixing package exports ([#737](https://github.com/codesandbox/sandpack/issues/737)) ([e96672a](https://github.com/codesandbox/sandpack/commit/e96672a4abb8f09234126b95bdf0bb641fdbad91)) + +## [2.0.7](https://github.com/codesandbox/sandpack/compare/v2.0.6...v2.0.7) (2023-02-20) + +### Bug Fixes + +- Invalid esm package exports ([#725](https://github.com/codesandbox/sandpack/issues/725)) ([44b05ec](https://github.com/codesandbox/sandpack/commit/44b05ecc5a2d322473f06e8eef8ad49afe3c2d20)), closes [#724](https://github.com/codesandbox/sandpack/issues/724) + +## [2.0.1](https://github.com/codesandbox/sandpack/compare/v1.20.9...v2.0.1) (2023-02-16) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [1.12.1](https://github.com/codesandbox/sandpack/compare/v1.12.0...v1.12.1) (2022-10-04) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [1.10.1](https://github.com/codesandbox/sandpack/compare/v1.10.0...v1.10.1) (2022-10-04) + +### Bug Fixes + +- **templates:** move dependencies to a package json ([#594](https://github.com/codesandbox/sandpack/issues/594)) ([441d5a5](https://github.com/codesandbox/sandpack/commit/441d5a5182f162d343e4ad56cb6321e3e839d984)) + +## [1.8.7](https://github.com/codesandbox/sandpack/compare/v1.8.6...v1.8.7) (2022-09-30) + +### Bug Fixes + +- **files:** normalize paths ([#595](https://github.com/codesandbox/sandpack/issues/595)) ([71a2044](https://github.com/codesandbox/sandpack/commit/71a2044f61ae32f69822f8eefdaae246629ef400)) + +## [1.8.5](https://github.com/codesandbox/sandpack/compare/v1.8.4...v1.8.5) (2022-09-28) + +### Bug Fixes + +- **client-navigation:** make sure iframe is using the right location on refreshing ([#587](https://github.com/codesandbox/sandpack/issues/587)) ([317d456](https://github.com/codesandbox/sandpack/commit/317d456d5b137e795bb6e7bfa86fb92803c5afd3)) + +# [1.7.0](https://github.com/codesandbox/sandpack/compare/v1.6.0...v1.7.0) (2022-08-31) + +### Features + +- **SandpackTests:** Add SandpackTests component ([#562](https://github.com/codesandbox/sandpack/issues/562)) ([1191f82](https://github.com/codesandbox/sandpack/commit/1191f82c643356a3ff5729edc81ef0b501f81edc)) + +## [1.5.4](https://github.com/codesandbox/sandpack/compare/v1.5.3...v1.5.4) (2022-08-22) + +### Bug Fixes + +- **client:** do not set default credentials for custom npm requests ([4ce7be9](https://github.com/codesandbox/sandpack/commit/4ce7be95e771339ebcbdd4868c04d3c4e2cb2d77)) + +## [1.5.3](https://github.com/codesandbox/sandpack/compare/v1.5.2...v1.5.3) (2022-08-22) + +### Bug Fixes + +- **client:** custom registry should include the credentials only for csb urls ([#555](https://github.com/codesandbox/sandpack/issues/555)) ([de1e424](https://github.com/codesandbox/sandpack/commit/de1e424092525f2e9fa013c35ec8c961a4f1ced7)) + +# [1.4.0](https://github.com/codesandbox/sandpack/compare/v1.3.5...v1.4.0) (2022-08-11) + +### Features + +- **custom-setup:** introduce custom npm registries ([#542](https://github.com/codesandbox/sandpack/issues/542)) ([1fd8b99](https://github.com/codesandbox/sandpack/commit/1fd8b997e3e95bc76026e3de1a5c267859d92c82)) + +## [1.3.2](https://github.com/codesandbox/sandpack/compare/v1.3.1...v1.3.2) (2022-07-29) + +### Bug Fixes + +- **client:** update bundler ([bb6d9c1](https://github.com/codesandbox/sandpack/commit/bb6d9c175568b26f51a11b84e44ceba7fcf3d74c)) + +## [1.2.2](https://github.com/codesandbox/sandpack/compare/v1.2.1...v1.2.2) (2022-06-29) + +### Bug Fixes + +- **codeeditor:** test-id should not be included in the bundler ([#521](https://github.com/codesandbox/sandpack/issues/521)) ([bf9cc21](https://github.com/codesandbox/sandpack/commit/bf9cc21827b69374914f416e03f06832d444af24)) + +## [1.2.1](https://github.com/codesandbox/sandpack/compare/v1.2.0...v1.2.1) (2022-06-27) + +### Bug Fixes + +- **global-listeners:** doesn't unsubscribe all listener unexpectedly ([#516](https://github.com/codesandbox/sandpack/issues/516)) ([7e65f6e](https://github.com/codesandbox/sandpack/commit/7e65f6ef3df52dfcd30be4181257ad3dadbfac53)) + +## [1.1.6](https://github.com/codesandbox/sandpack/compare/v1.1.5...v1.1.6) (2022-06-20) + +### Bug Fixes + +- **client/react:** make Template more accessible and do a deep equal on react context ([#504](https://github.com/codesandbox/sandpack/issues/504)) ([31980f8](https://github.com/codesandbox/sandpack/commit/31980f86e40d4cd09e586eaa004df8073e02d6e0)) + +## [1.1.3](https://github.com/codesandbox/sandpack/compare/v1.1.2...v1.1.3) (2022-06-07) + +### Bug Fixes + +- **file-explorer:** adds property not to show hidden files ([#488](https://github.com/codesandbox/sandpack/issues/488)) ([1048fe9](https://github.com/codesandbox/sandpack/commit/1048fe93b7f3be2d54cd9d35ac64271e1ea613fe)) + +# [1.1.0](https://github.com/codesandbox/sandpack/compare/v1.0.4...v1.1.0) (2022-05-31) + +### Features + +- **client:** refactor iFrame fs protocol ([#483](https://github.com/codesandbox/sandpack/issues/483)) ([28f93d0](https://github.com/codesandbox/sandpack/commit/28f93d05b978ae59655d4c574a1393ce4b9d6e53)) + +## [1.0.4](https://github.com/codesandbox/sandpack/compare/v1.0.3...v1.0.4) (2022-05-27) + +### Bug Fixes + +- **release-script:** trigger release ([#480](https://github.com/codesandbox/sandpack/issues/480)) ([46890fd](https://github.com/codesandbox/sandpack/commit/46890fdc6748f997cad38a39860b573869af1c60)) + +## [1.0.2](https://github.com/codesandbox/sandpack/compare/v1.0.1...v1.0.2) (2022-05-26) + +### Bug Fixes + +- **package registry:** trigger deploy ([#475](https://github.com/codesandbox/sandpack/issues/475)) ([551c7c0](https://github.com/codesandbox/sandpack/commit/551c7c08898e5a49ceae36572b3e16bff5e9d64c)) + +# [1.0.0](https://github.com/codesandbox/sandpack/compare/v0.19.10...v1.0.0) (2022-05-25) + +### Features + +- **react/client:** BREAKING CHANGES ([#375](https://github.com/codesandbox/sandpack/issues/375)) ([20a8993](https://github.com/codesandbox/sandpack/commit/20a899337343e35a8d8e0b4e00c42e7190625747)) + +## [0.19.9](https://github.com/codesandbox/sandpack/compare/v0.19.8...v0.19.9) (2022-05-23) + +### Bug Fixes + +- **client:** console message methods ([1b76dcf](https://github.com/codesandbox/sandpack/commit/1b76dcf5ccfd0db61cda8d70329424cdf27116ad)) + +## [0.19.8](https://github.com/codesandbox/sandpack/compare/v0.19.7...v0.19.8) (2022-05-23) + +### Bug Fixes + +- **sandpack messages:** add console type ([3edcb4d](https://github.com/codesandbox/sandpack/commit/3edcb4d11238f47ecbc286a8535205579856d3f3)) + +## [0.19.1](https://github.com/codesandbox/sandpack/compare/v0.19.0...v0.19.1) (2022-04-26) + +### Bug Fixes + +- **client:** update bundler version ([#450](https://github.com/codesandbox/sandpack/issues/450)) ([1b6a663](https://github.com/codesandbox/sandpack/commit/1b6a663d8d7ff0bf3c3449b4aa0bfd675e37a221)) + +# [0.19.0](https://github.com/codesandbox/sandpack/compare/v0.18.2...v0.19.0) (2022-04-21) + +### Features + +- **template:** add solidjs (beta) ([#447](https://github.com/codesandbox/sandpack/issues/447)) ([7b03882](https://github.com/codesandbox/sandpack/commit/7b038827add0001b460af7574e12e4c664a075d2)) + +# [0.18.0](https://github.com/codesandbox/sandpack/compare/v0.17.1...v0.18.0) (2022-03-31) + +### Features + +- file-resolver protocol error handling ([#427](https://github.com/codesandbox/sandpack/issues/427)) ([c3b3cca](https://github.com/codesandbox/sandpack/commit/c3b3cca98ca4aba2c0744545969683e41d963ab4)) + +# [0.17.0](https://github.com/codesandbox/sandpack/compare/v0.16.1...v0.17.0) (2022-03-30) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [0.16.1](https://github.com/codesandbox/sandpack/compare/v0.16.0...v0.16.1) (2022-03-29) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [0.16.0](https://github.com/codesandbox/sandpack/compare/v0.15.2...v0.16.0) (2022-03-27) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [0.15.2](https://github.com/codesandbox/sandpack/compare/v0.15.1...v0.15.2) (2022-03-18) + +### Bug Fixes + +- **loglevel:** set default ([#418](https://github.com/codesandbox/sandpack/issues/418)) ([abf0243](https://github.com/codesandbox/sandpack/commit/abf0243e5f106888ac3829daa6a5e6d6dd4f41b5)) + +# [0.15.0](https://github.com/codesandbox/sandpack/compare/v0.14.9...v0.15.0) (2022-03-16) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [0.14.8](https://github.com/codesandbox/sandpack/compare/v0.14.7...v0.14.8) (2022-03-11) + +### Bug Fixes + +- **client:** prevent add route into the main page browser history ([#407](https://github.com/codesandbox/sandpack/issues/407)) ([1e5230a](https://github.com/codesandbox/sandpack/commit/1e5230af5dff6c9afec8a96d1b9281cc585a826d)) + +## [0.14.4](https://github.com/codesandbox/sandpack/compare/v0.14.3...v0.14.4) (2022-03-07) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [0.14.0](https://github.com/codesandbox/sandpack/compare/v0.13.15...v0.14.0) (2022-02-18) + +### Features + +- add loglevel to sandpack opts ([#378](https://github.com/codesandbox/sandpack/issues/378)) ([a3216e8](https://github.com/codesandbox/sandpack/commit/a3216e8f4940373df87e938148632e46cb661b4f)) + +## [0.13.15](https://github.com/codesandbox/sandpack/compare/v0.13.14...v0.13.15) (2022-02-11) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [0.13.7](https://github.com/codesandbox/sandpack/compare/v0.13.6...v0.13.7) (2022-01-26) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [0.13.5](https://github.com/codesandbox/sandpack/compare/v0.13.4...v0.13.5) (2022-01-21) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [0.13.0](https://github.com/codesandbox/sandpack/compare/v0.12.0...v0.13.0) (2022-01-14) + +### Features + +- **files:** read-only mode ([#300](https://github.com/codesandbox/sandpack/issues/300)) ([9d5d1bf](https://github.com/codesandbox/sandpack/commit/9d5d1bfc3ac0d21d57958ee61057a706762701f2)) + +# [0.12.0](https://github.com/codesandbox/sandpack/compare/v0.11.0...v0.12.0) (2022-01-11) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [0.11.0](https://github.com/codesandbox/sandpack/compare/v0.10.12...v0.11.0) (2022-01-11) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [0.10.11](https://github.com/codesandbox/sandpack/compare/v0.10.10...v0.10.11) (2022-01-05) + +### Bug Fixes + +- **react-devtools:** legacy mode ([#272](https://github.com/codesandbox/sandpack/issues/272)) ([4891ba4](https://github.com/codesandbox/sandpack/commit/4891ba4bdf5ad8ed1aed1f6a37cc4b8e5a94b8e2)) + +## [0.10.3](https://github.com/codesandbox/sandpack/compare/v0.10.2...v0.10.3) (2021-12-14) + +### Bug Fixes + +- lint errors ([#234](https://github.com/codesandbox/sandpack/issues/234)) ([2d51830](https://github.com/codesandbox/sandpack/commit/2d518309cfd86222078fde25d399ea12258b3493)) + +# [0.10.0](https://github.com/codesandbox/sandpack/compare/v0.9.14...v0.10.0) (2021-12-09) + +### Features + +- **react:** react devtool ([#236](https://github.com/codesandbox/sandpack/issues/236)) ([a67e1b2](https://github.com/codesandbox/sandpack/commit/a67e1b2ccfc38b01ad78d2d7f518148cf94eb15d)) + +## [0.9.13](https://github.com/codesandbox/sandpack/compare/v0.9.12...v0.9.13) (2021-12-08) + +### Bug Fixes + +- **client:** support reactdevtools ([7f0373f](https://github.com/codesandbox/sandpack/commit/7f0373f328d60229904bbebab0329718616b59bb)) + +## [0.9.9](https://github.com/codesandbox/sandpack/compare/v0.9.8...v0.9.9) (2021-12-03) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [0.9.8](https://github.com/codesandbox/sandpack/compare/v0.9.7...v0.9.8) (2021-12-02) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [0.9.0](https://github.com/codesandbox/sandpack/compare/v0.8.0...v0.9.0) (2021-11-25) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [0.8.0](https://github.com/codesandbox/sandpack/compare/v0.7.3...v0.8.0) (2021-11-25) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [0.7.1](https://github.com/codesandbox/sandpack/compare/v0.7.0...v0.7.1) (2021-11-24) + +### Bug Fixes + +- **bundler:** make sure transpiled files are cut ([ef11a7c](https://github.com/codesandbox/sandpack/commit/ef11a7c5178e6d41200ed0b4aa157c6c73096eba)) + +# [0.7.0](https://github.com/codesandbox/sandpack/compare/v0.6.0...v0.7.0) (2021-11-23) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [0.6.0](https://github.com/codesandbox/sandpack/compare/v0.5.4...v0.6.0) (2021-11-22) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [0.5.0](https://github.com/codesandbox/sandpack/compare/v0.4.1...v0.5.0) (2021-11-19) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [0.4.0](https://github.com/codesandbox/sandpack/compare/v0.3.10...v0.4.0) (2021-11-18) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [0.3.7](https://github.com/codesandbox/sandpack/compare/v0.3.6...v0.3.7) (2021-11-16) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [0.3.3](https://github.com/codesandbox/sandpack/compare/v0.3.2...v0.3.3) (2021-11-15) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [0.3.2](https://github.com/codesandbox/sandpack/compare/v0.3.1...v0.3.2) (2021-11-15) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [0.3.0](https://github.com/codesandbox/sandpack/compare/v0.2.3...v0.3.0) (2021-11-15) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +# [0.2.0](https://github.com/codesandbox/sandpack/compare/v0.1.20...v0.2.0) (2021-11-10) + +### Features + +- **template:** add react typescript ([#114](https://github.com/codesandbox/sandpack/issues/114)) ([96aaac8](https://github.com/codesandbox/sandpack/commit/96aaac86afc2287a1e96fa95a9836d156a4bc9de)) + +## [0.1.20](https://github.com/codesandbox/sandpack/compare/v0.1.19...v0.1.20) (2021-11-08) + +### Bug Fixes + +- **codemirror:** upgrade dependencies ([#125](https://github.com/codesandbox/sandpack/issues/125)) ([7cbf7f1](https://github.com/codesandbox/sandpack/commit/7cbf7f1aa8f07b4826eb8ebbeb1ca5d868b5c4df)) + +## [0.1.19](https://github.com/codesandbox/sandpack/compare/v0.1.18...v0.1.19) (2021-11-04) + +### Bug Fixes + +- **bundler:** reduce retry count for jsdelivr if it fails to load ([0712fe1](https://github.com/codesandbox/sandpack/commit/0712fe16ec25df8ca420e3e36c22742711ec2d0b)) + +## [0.1.18](https://github.com/codesandbox/sandpack/compare/v0.1.17...v0.1.18) (2021-11-04) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [0.1.17](https://github.com/codesandbox/sandpack/compare/v0.1.16...v0.1.17) (2021-11-04) + +**Note:** Version bump only for package @codesandbox/sandpack-client + +## [0.1.16](https://github.com/codesandbox/sandpack/compare/v0.1.15...v0.1.16) (2021-11-03) + +**Note:** Version bump only for package @codesandbox/sandpack-client diff --git a/sandpack-environments/README.md b/sandpack-environments/README.md new file mode 100644 index 000000000..aae84a5f8 --- /dev/null +++ b/sandpack-environments/README.md @@ -0,0 +1,26 @@ +Component toolkit for live running code editing experiences + +# Sandpack client + +This is a small foundation package that sits on top of the bundler. It is +framework agnostic and facilitates the handshake between your context and the bundler iframe. + +```js +import { loadSandpackClient } from "@codesandbox/sandpack-client"; + +const main = async () => { + const client = await loadSandpackClient("#preview", { + files: { + "/index.js": { + code: `console.log(require('uuid'))`, + }, + }, + entry: "/index.js", + dependencies: { + uuid: "latest", + }, + }); +} +``` + +[Read more](https://sandpack.codesandbox.io/docs/advanced-usage/client) diff --git a/sandpack-environments/babel.config.js b/sandpack-environments/babel.config.js new file mode 100644 index 000000000..89a4577b9 --- /dev/null +++ b/sandpack-environments/babel.config.js @@ -0,0 +1,7 @@ +module.exports = { + presets: [ + ["@babel/preset-env", { targets: { node: "current" } }], + "@babel/preset-react", + "@babel/preset-typescript", + ], +}; diff --git a/sandpack-environments/gulpfile.js b/sandpack-environments/gulpfile.js new file mode 100644 index 000000000..660ce8d69 --- /dev/null +++ b/sandpack-environments/gulpfile.js @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const del = require("del"); +const gulp = require("gulp"); +var Transform = require("stream").Transform; + +function removeSourcemaps() { + var transformStream = new Transform({ objectMode: true }); + + transformStream._transform = function (file, encoding, callback) { + if (file.isNull()) { + return callback(null, file); + } + + if (file.isStream()) { + return callback(": Streams not supported!", undefined); + } + + let contents = file.contents.toString(encoding); + + const lines = contents.split("\n"); + const lastLine = lines[lines.length - 1]; + if (lastLine.startsWith("//# sourceMappingURL=")) { + lines.pop(); + } + + contents = lines.join("\n"); + + file.contents = Buffer.from(contents, encoding); + + callback(null, file); + }; + + return transformStream; +} + +const dist = "./sandpack/"; +const paths = process.env.CI + ? ["../bundler/**/!(*.map)", "!../bundler/public/**"] + : [ + "../../codesandbox-client/www/**/!(*.map)", + "!../../codesandbox-client/www/public/**", + ]; + +const remove = () => del(dist); +const copyFolder = () => + gulp + .src(paths, { matchBase: true }) + .pipe(removeSourcemaps()) + .pipe(gulp.dest(dist)); + +exports["default"] = gulp.series(remove, copyFolder); diff --git a/sandpack-environments/package.json b/sandpack-environments/package.json new file mode 100644 index 000000000..9c5aee6d8 --- /dev/null +++ b/sandpack-environments/package.json @@ -0,0 +1,69 @@ +{ + "name": "@codesandbox/sandpack-environments", + "version": "0.1.0", + "description": "", + "keywords": [], + "repository": { + "type": "git", + "url": "https://github.com/codesandbox/sandpack" + }, + "license": "Apache-2.0", + "author": "CodeSandbox", + "sideEffects": false, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + "./clients/runtime": { + "types": "./dist/clients/runtime/index.d.ts", + "import": "./dist/clients/runtime/index.mjs", + "require": "./dist/clients/runtime/index.js" + }, + "./clients/static": { + "types": "./dist/clients/static/index.d.ts", + "import": "./dist/clients/static/index.mjs", + "require": "./dist/clients/static/index.js" + }, + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "clean": "rm -rf dist sandpack src/clients/node/inject-scripts/dist", + "prebuild": "yarn run clean", + "test": "jest .", + "lint": "eslint '**/*.ts?(x)' --fix", + "build": "rollup -c --bundleConfigAsCjs", + "build:publish": "yarn build && gulp", + "build:bundler": "gulp", + "format": "prettier --write '**/*.{ts,tsx,js,jsx}'", + "format:check": "prettier --check '**/*.{ts,tsx}'", + "dev": "yarn build -- --watch", + "typecheck": "tsc --noEmit --skipLibCheck" + }, + "files": [ + "dist", + "esm", + "sandpack", + "package.json", + "README.md" + ], + "dependencies": { + "@codesandbox/sdk": "^0.11.1", + "buffer": "^6.0.3", + "dequal": "^2.0.2", + "mime-db": "^1.54.0", + "outvariant": "1.4.0", + "static-browser-server": "1.0.3" + }, + "devDependencies": { + "@types/mime-db": "^1.43.5", + "@types/node": "^9.3.0", + "console-feed": "3.3.0", + "del": "^6.0.0", + "gulp": "^4.0.2", + "typescript": "^5.2.2" + } +} diff --git a/sandpack-environments/rollup.config.js b/sandpack-environments/rollup.config.js new file mode 100644 index 000000000..4721e24b1 --- /dev/null +++ b/sandpack-environments/rollup.config.js @@ -0,0 +1,70 @@ +import commonjs from "@rollup/plugin-commonjs"; +import { nodeResolve } from "@rollup/plugin-node-resolve"; +import replace from "@rollup/plugin-replace"; +import terser from "@rollup/plugin-terser"; +import typescript from "@rollup/plugin-typescript"; +import { string } from "rollup-plugin-string"; + +import pkg from "./package.json"; + +const configs = [ + { + input: "src/inject-scripts/consoleHook.ts", + output: { + file: "src/inject-scripts/dist/consoleHook.js", + format: "es", + }, + plugins: [ + typescript({ + tsconfig: "./tsconfig.json", + compilerOptions: { declaration: false }, + }), + commonjs(), + nodeResolve(), + terser({ compress: { passes: 2 } }), + ], + external: [], + }, + + { + input: { + index: "src/index.ts", + "clients/static/index": "src/clients/static/index.ts", + }, + output: [ + { + dir: "dist", + format: "cjs", + }, + { + dir: "dist", + chunkFileNames: "[name]-[hash].mjs", + entryFileNames: "[name].mjs", + format: "es", + }, + ], + + plugins: [ + typescript({ + tsconfig: "./tsconfig.json", + compilerOptions: { declaration: true, declarationDir: "dist" }, + }), + string({ include: "**/dist/consoleHook.js" }), + replace({ + preventAssignment: true, + values: { + global: "globalThis", + "process.env.CODESANDBOX_ENV": `"${process.env.CODESANDBOX_ENV}"`, + "process.env.PACKAGE_VERSION": `"${pkg.version}"`, + }, + }), + ], + external: Object.keys({ + ...(pkg.dependencies || {}), + ...(pkg.devDependencies || {}), + ...(pkg.peerDependencies || {}), + }).concat("@codesandbox/sdk/browser"), + }, +]; + +export default configs; diff --git a/sandpack-environments/src/InMemoryFileSystem.ts b/sandpack-environments/src/InMemoryFileSystem.ts new file mode 100644 index 000000000..f2fabdcfd --- /dev/null +++ b/sandpack-environments/src/InMemoryFileSystem.ts @@ -0,0 +1,319 @@ +import type { FileContent } from "./sandpack-bundler-types"; +import type { SandpackFileSystem } from "./types"; +import { normalizePath } from "./utils"; + +interface Directory { + name: string; + path: string; + files: Set; + subdirectories: Set; +} + +export class InMemoryFileSystem implements SandpackFileSystem { + private files: Record = {}; + private filesMetadata: Record = {}; + private directories: Record = {}; + private watchers: Record void>> = {}; + private globalWatchers: Set<() => void> = new Set(); + + constructor() { + // Initialize root directory + this.directories["/"] = { + name: "/", + path: "/", + files: new Set(), + subdirectories: new Set(), + }; + } + + private ensureDirectoryExists(path: string): void { + const normalizedPath = normalizePath(path); + if (normalizedPath === "/") return; // Root always exists + + const parts = normalizedPath.split("/").filter(Boolean); + let currentPath = "/"; + + // Create parent directories if they don't exist + for (let i = 0; i < parts.length; i++) { + const dirName = parts[i]; + const nextPath = `${currentPath}${dirName}/`; + + if (!this.directories[nextPath]) { + this.directories[nextPath] = { + name: dirName, + path: nextPath, + files: new Set(), + subdirectories: new Set(), + }; + + // Add to parent's subdirectories + this.directories[currentPath].subdirectories.add(nextPath); + } + + currentPath = nextPath; + } + } + + private getDirectoryForFile(filePath: string): string { + const normalizedPath = normalizePath(filePath); + const lastSlashIndex = normalizedPath.lastIndexOf("/"); + if (lastSlashIndex <= 0) return "/"; // Root directory + return normalizedPath.substring(0, lastSlashIndex + 1); + } + + async writeFile(path: string, content: string) { + const normalizedPath = normalizePath(path); + const dirPath = this.getDirectoryForFile(normalizedPath); + + // Ensure parent directory exists + this.ensureDirectoryExists(dirPath); + + // Add file to directory + this.directories[dirPath].files.add(normalizedPath); + + // Store file content + this.files[normalizedPath] = content; + + // Notify watchers + if (this.watchers[normalizedPath]) { + this.watchers[normalizedPath].forEach((callback) => callback()); + } + + // Notify global watchers + this.notifyGlobalWatchers(); + } + + writeFileMetadata(path: string, metadata: object): void { + const normalizedPath = normalizePath(path); + + this.filesMetadata[normalizedPath] = metadata; + } + + readFileMetadata(path: string): object { + const normalizedPath = normalizePath(path); + + if (!this.filesMetadata[normalizedPath]) { + throw new Error(`ENOENT: no such file '${path}'`); + } + + return this.filesMetadata[normalizedPath]; + } + + async readFile(path: string) { + const normalizedPath = normalizePath(path); + + if (!this.files[normalizedPath]) { + throw new Error(`ENOENT: no such file '${path}'`); + } + + return this.files[normalizedPath]; + } + + async readDirectory(path: string) { + const normalizedPath = normalizePath(path); + const directoryPath = normalizedPath.endsWith("/") + ? normalizedPath + : `${normalizedPath}/`; + + if (!this.directories[directoryPath]) { + throw new Error(`ENOENT: no such directory '${path}'`); + } + + const directory = this.directories[directoryPath]; + + // Get file entries + const fileEntries = Array.from(directory.files).map((filePath) => { + const parts = filePath.split("/"); + return { + name: parts[parts.length - 1], + type: "file" as const, + }; + }); + + // Get directory entries + const dirEntries = Array.from(directory.subdirectories).map((dirPath) => { + // Remove trailing slash and get the last part + const name = dirPath.endsWith("/") ? dirPath.slice(0, -1) : dirPath; + const parts = name.split("/"); + return { + name: parts[parts.length - 1], + type: "directory" as const, + }; + }); + + return [...fileEntries, ...dirEntries]; + } + + async createDirectory(path: string): Promise { + const normalizedPath = normalizePath(path); + const directoryPath = normalizedPath.endsWith("/") + ? normalizedPath + : `${normalizedPath}/`; + + this.ensureDirectoryExists(directoryPath); + + // Notify watchers + if (this.watchers[directoryPath]) { + this.watchers[directoryPath].forEach((callback) => callback()); + } + + // Notify global watchers + this.notifyGlobalWatchers(); + } + + async deleteDirectory(path: string): Promise { + const normalizedPath = normalizePath(path); + const directoryPath = normalizedPath.endsWith("/") + ? normalizedPath + : `${normalizedPath}/`; + + if (!this.directories[directoryPath]) { + return; // Directory doesn't exist, nothing to delete + } + + // Get all files in this directory and subdirectories + const filesToDelete = this.getAllFilesInDirectory(directoryPath); + + // Delete all files + for (const filePath of filesToDelete) { + delete this.files[filePath]; + + // Notify watchers + if (this.watchers[filePath]) { + this.watchers[filePath].forEach((callback) => callback()); + } + } + + // Delete from parent's subdirectories + const parentPath = this.getParentDirectoryPath(directoryPath); + if (parentPath && this.directories[parentPath]) { + this.directories[parentPath].subdirectories.delete(directoryPath); + } + + // Delete the directory and all subdirectories + const dirsToDelete = this.getAllSubdirectories(directoryPath); + for (const dirPath of dirsToDelete) { + delete this.directories[dirPath]; + + // Notify watchers + if (this.watchers[dirPath]) { + this.watchers[dirPath].forEach((callback) => callback()); + } + } + + // Notify global watchers + this.notifyGlobalWatchers(); + } + + private getAllFilesInDirectory(dirPath: string): string[] { + const result: string[] = []; + const dir = this.directories[dirPath]; + + if (!dir) return result; + + // Add direct files + result.push(...Array.from(dir.files)); + + // Add files from subdirectories + for (const subdir of dir.subdirectories) { + result.push(...this.getAllFilesInDirectory(subdir)); + } + + return result; + } + + private getAllSubdirectories(dirPath: string): string[] { + const result: string[] = [dirPath]; + const dir = this.directories[dirPath]; + + if (!dir) return result; + + // Add subdirectories recursively + for (const subdir of dir.subdirectories) { + result.push(...this.getAllSubdirectories(subdir)); + } + + return result; + } + + private getParentDirectoryPath(dirPath: string): string | null { + if (dirPath === "/") return null; // Root has no parent + + const parts = dirPath.split("/").filter(Boolean); + if (parts.length === 0) return null; + + parts.pop(); // Remove last part + return parts.length === 0 ? "/" : `/${parts.join("/")}/`; + } + + async deleteFile(path: string): Promise { + const normalizedPath = normalizePath(path); + + if (!this.files[normalizedPath]) { + return; // File doesn't exist, nothing to delete + } + + // Remove from directory + const dirPath = this.getDirectoryForFile(normalizedPath); + if (this.directories[dirPath]) { + this.directories[dirPath].files.delete(normalizedPath); + } + + // Delete file + delete this.files[normalizedPath]; + delete this.filesMetadata[normalizedPath]; + + // Notify watchers + if (this.watchers[normalizedPath]) { + this.watchers[normalizedPath].forEach((callback) => callback()); + } + + // Notify global watchers + this.notifyGlobalWatchers(); + } + + private notifyGlobalWatchers(): void { + this.globalWatchers.forEach((callback) => callback()); + } + + watch(callback: () => void): () => void { + this.globalWatchers.add(callback); + + // Return unwatch function + return () => { + this.globalWatchers.delete(callback); + }; + } + + watchDirectory(path: string, callback: () => void): () => void { + const normalizedPath = normalizePath(path); + if (!this.watchers[normalizedPath]) { + this.watchers[normalizedPath] = new Set(); + } + + this.watchers[normalizedPath].add(callback); + + return () => { + this.watchers[normalizedPath].delete(callback); + }; + } + + dispose() { + // Clean up watchers + for (const path in this.watchers) { + this.watchers[path].clear(); + } + + this.watchers = {}; + this.globalWatchers.clear(); + this.files = {}; + this.directories = { + "/": { + name: "/", + path: "/", + files: new Set(), + subdirectories: new Set(), + }, + }; + } +} diff --git a/sandpack-environments/src/clients/bundler/BundlerPreview.ts b/sandpack-environments/src/clients/bundler/BundlerPreview.ts new file mode 100644 index 000000000..232810670 --- /dev/null +++ b/sandpack-environments/src/clients/bundler/BundlerPreview.ts @@ -0,0 +1,440 @@ +import type { InMemoryFileSystem } from "../../InMemoryFileSystem"; +import type { + SandpackPreviewMessage, + SandpackPreviewStatus, +} from "../../types"; + +import Protocol from "./file-resolver-protocol"; +import { IFrameProtocol } from "./iframe-protocol.js"; +import { EXTENSIONS_MAP } from "./mime"; +import type { + IPreviewRequestMessage, + IPreviewResponseMessage, + ListenerFunction, + Modules, + SandpackBundlerFile, + SandpackBundlerFiles, + UnsubscribeFunction, +} from "./types"; +import { + CHANNEL_NAME, + SandpackLogLevel, + type BundlerState, + type SandpackError, + type SandpackMessage, +} from "./types"; +import { + ensureValidBundlerFiles, + extractErrorDetails, + getExtension, + getTemplate, + readAllFiles, +} from "./utils"; + +import type { SandpackBundlerEnvironmentOptions } from "./index"; + +const SUFFIX_PLACEHOLDER = "-{{suffix}}"; + +const BUNDLER_URL = + process.env.CODESANDBOX_ENV === "development" + ? "http://localhost:3000/" + : `https://${process.env.PACKAGE_VERSION?.replace( + /\./g, + "-" + )}${SUFFIX_PLACEHOLDER}-sandpack.codesandbox.io/`; + +export class SandpackBundlerPreview { + private disposers = new Set<() => void>(); + private iframeMessageSubscribers = new Set< + (message: SandpackPreviewMessage) => void + >(); + private statusSubscribers = new Set< + (status: SandpackPreviewStatus) => void + >(); + private iframe: HTMLIFrameElement; + private bundlerURL: string; + private bundlerState?: BundlerState; + private errors: SandpackError[] = []; + private _status: SandpackPreviewStatus = { + current: "LOADING", + progress: [], + }; + get status() { + return this._status; + } + set status(newStatus) { + this._status = newStatus; + this.statusSubscribers.forEach((subscriber) => { + subscriber(newStatus); + }); + } + private fileResolverProtocol?: Protocol; + private iframeProtocol: IFrameProtocol; + constructor( + iframe: HTMLIFrameElement, + private options: SandpackBundlerEnvironmentOptions, + private fs: InMemoryFileSystem + ) { + this.bundlerURL = this.createBundlerURL(); + this.iframe = this.configureIframe(iframe); + this.iframeProtocol = this.configureIframeProtocol(); + this.setLocationURLIntoIFrame(); + } + private createBundlerURL() { + let bundlerURL = this.options.bundlerURL || BUNDLER_URL; + + // if it's a custom, skip the rest + if (this.options.bundlerURL) { + return bundlerURL; + } + + if (this.options.teamId) { + bundlerURL = + bundlerURL.replace("https://", "https://" + this.options.teamId + "-") + + `?cache=${Date.now()}`; + } + + if (this.options.experimental_enableServiceWorker) { + const suffixes: string[] = []; + suffixes.push(Math.random().toString(36).slice(4)); + + bundlerURL = bundlerURL.replace( + SUFFIX_PLACEHOLDER, + `-${ + this.options.experimental_stableServiceWorkerId ?? suffixes.join("-") + }` + ); + } else { + bundlerURL = bundlerURL.replace(SUFFIX_PLACEHOLDER, ""); + } + + return bundlerURL; + } + + private configureIframeProtocol() { + const iframeProtocol = new IFrameProtocol(this.iframe, this.bundlerURL); + + this.disposers.add( + iframeProtocol.globalListen((mes: SandpackMessage) => { + if (mes.type !== "initialized" || !this.iframe.contentWindow) { + return; + } + + iframeProtocol.register(); + + if (this.options.fileResolver) { + this.fileResolverProtocol = new Protocol( + "fs", + async (data) => { + if (data.method === "isFile") { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.options.fileResolver!.isFile(data.params[0]); + } else if (data.method === "readFile") { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.options.fileResolver!.readFile(data.params[0]); + } else { + throw new Error("Method not supported"); + } + }, + this.iframeProtocol + ); + } + + this.updateSandbox(); + }) + ); + + this.disposers.add( + iframeProtocol.channelListen((mes: SandpackMessage) => { + switch (mes.type) { + case "start": { + this.errors = []; + this.status = { + current: "LOADING", + progress: [], + }; + break; + } + case "status": { + if (this.status.current === "LOADING") { + this.status.progress.push(mes.status); + } + break; + } + case "action": { + if (mes.action === "show-error") { + this.errors = [...this.errors, extractErrorDetails(mes)]; + } + break; + } + case "done": { + this.status = { + current: "READY", + }; + break; + } + case "state": { + this.bundlerState = mes.state; + break; + } + } + }) + ); + + if (this.options.experimental_enableServiceWorker) { + this.serviceWorkerHandshake(); + } + + return iframeProtocol; + } + + setLocationURLIntoIFrame(): void { + const urlSource = this.options.startRoute + ? new URL(this.options.startRoute, this.bundlerURL).toString() + : this.bundlerURL; + + this.iframe.contentWindow?.location.replace(urlSource); + this.iframe.src = urlSource; + } + + private configureIframe(iframe: HTMLIFrameElement) { + if (!iframe.getAttribute("sandbox")) { + iframe.setAttribute( + "sandbox", + "allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts allow-downloads allow-pointer-lock" + ); + + iframe.setAttribute( + "allow", + "accelerometer; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; clipboard-read; clipboard-write; xr-spatial-tracking;" + ); + } + + return iframe; + } + + private serviceWorkerHandshake() { + const channel = new MessageChannel(); + + const iframeContentWindow = this.iframe.contentWindow; + if (!iframeContentWindow) { + throw new Error("Could not get iframe contentWindow"); + } + + const port = channel.port1; + port.onmessage = (evt: MessageEvent) => { + if (typeof evt.data === "object" && evt.data.$channel === CHANNEL_NAME) { + switch (evt.data.$type) { + case "preview/ready": + // no op for now + break; + case "preview/request": + this.handleWorkerRequest(evt.data, port); + + break; + } + } + }; + + const sendMessage = () => { + const initMsg = { + $channel: CHANNEL_NAME, + $type: "preview/init", + }; + + iframeContentWindow.postMessage(initMsg, "*", [channel.port2]); + + this.iframe.removeEventListener("load", sendMessage); + }; + + this.iframe.addEventListener("load", sendMessage); + } + + public getTranspiledFiles = (): Promise< + Array<{ path: string; code: string }> + > => { + return new Promise((resolve) => { + const unsubscribe = this.listen((message) => { + if (message.type === "all-modules") { + resolve(message.data); + + unsubscribe(); + } + }); + + this.dispatch({ type: "get-modules" }); + }); + }; + + private async getFiles(): Promise { + const files = await readAllFiles(this.fs, "/", {}); + + return ensureValidBundlerFiles(files, this.options.entry); + } + + private async handleWorkerRequest( + request: IPreviewRequestMessage, + port: MessagePort + ) { + const notFound = () => { + const responseMessage: IPreviewResponseMessage = { + $channel: CHANNEL_NAME, + $type: "preview/response", + id: request.id, + headers: { + "Content-Type": "text/html; charset=utf-8", + }, + status: 404, + body: "File not found", + }; + + port.postMessage(responseMessage); + }; + try { + const filepath = new URL(request.url, this.bundlerURL).pathname; + + const headers: Record = {}; + + const files = await this.getFiles(); + let file = files[filepath]; + + if (!file) { + const modulesFromManager = await this.getTranspiledFiles(); + + file = modulesFromManager.find((item) => + item.path.endsWith(filepath) + ) as SandpackBundlerFile; + + if (!file) { + notFound(); + return; + } + } + + const body = file.code; + + if (!headers["Content-Type"]) { + const extension = getExtension(filepath); + const foundMimetype = EXTENSIONS_MAP.get(extension); + if (foundMimetype) { + headers["Content-Type"] = foundMimetype; + } + } + + const responseMessage: IPreviewResponseMessage = { + $channel: CHANNEL_NAME, + $type: "preview/response", + id: request.id, + headers, + status: 200, + body, + }; + + port.postMessage(responseMessage); + } catch (err) { + console.error(err); + notFound(); + } + } + + public dispatch(message: SandpackMessage): void { + /** + * Intercept "refresh" dispatch: this will make sure + * that the iframe is still in the location it's supposed to be. + * External links inside the iframe will change the location and + * prevent the user from navigating back. + */ + if (message.type === "refresh") { + this.setLocationURLIntoIFrame(); + + if (this.options.experimental_enableServiceWorker) { + this.serviceWorkerHandshake(); + } + } + + this.iframeProtocol.dispatch(message); + } + + public listen(listener: ListenerFunction): UnsubscribeFunction { + return this.iframeProtocol.channelListen(listener); + } + + onStatusChange( + callback: (status: SandpackPreviewStatus) => void + ): () => void { + this.statusSubscribers.add(callback); + return () => { + this.statusSubscribers.delete(callback); + }; + } + + async updateSandbox() { + const files = await this.getFiles(); + const packageJSON = JSON.parse(files["package.json"]?.code ?? "{}"); + + // TODO: The API currently finds the index file and loads it in the preview, but we + // need to manually fix this + // data.files["index.html"] = data.files["src/index.html"]; + + const modules: Modules = Object.keys(files).reduce( + (prev, next) => ({ + ...prev, + [next]: { + code: files[next].code, + path: next, + }, + }), + {} + ); + + // TODO move this to a common format + const normalizedModules = Object.keys(files).reduce( + (prev, next) => ({ + ...prev, + [next]: { + content: files[next].code, + path: next, + }, + }), + {} + ); + + const payload = { + ...this.options, + type: "compile", + codesandbox: true, + version: 3, + isInitializationCompile: true, + modules, + reactDevTools: this.options.reactDevTools, + externalResources: this.options.externalResources || [], + hasFileResolver: Boolean(this.options.fileResolver), + disableDependencyPreprocessing: + this.options.disableDependencyPreprocessing, + experimental_enableServiceWorker: + this.options.experimental_enableServiceWorker, + template: + this.options.bundlerType || getTemplate(packageJSON, normalizedModules), + showOpenInCodeSandbox: this.options.showOpenInCodeSandbox ?? true, + showErrorScreen: this.options.showErrorScreen ?? true, + showLoadingScreen: this.options.showLoadingScreen ?? false, + skipEval: this.options.skipEval || false, + clearConsoleDisabled: !this.options.clearConsoleOnFirstCompile, + logLevel: this.options.logLevel ?? SandpackLogLevel.Info, + customNpmRegistries: this.options.customNpmRegistries, + teamId: this.options.teamId, + sandboxId: this.options.sandboxId, + } as any; + + console.log({ payload }); + + this.dispatch(payload); + } + + dispose(): void { + this.disposers.forEach((dispose) => { + dispose(); + }); + this.disposers.clear(); + } +} diff --git a/sandpack-environments/src/clients/bundler/file-resolver-protocol.ts b/sandpack-environments/src/clients/bundler/file-resolver-protocol.ts new file mode 100644 index 000000000..51af7e225 --- /dev/null +++ b/sandpack-environments/src/clients/bundler/file-resolver-protocol.ts @@ -0,0 +1,58 @@ +/** + * This file is a copy of the resolver from the `codesandbox-api` package. + * We wanted to avoid to reference codesandbox-api because of the code that runs on load in the package. + * The plan is to take some time and refactor codesandbox-api into what it was supposed to be in the first place, + * an abstraction over the actions that can be dispatched between the bundler and the iframe. + */ + +import type { IFrameProtocol } from "./iframe-protocol"; +import type { + UnsubscribeFunction, + ProtocolRequestMessage, + ProtocolResultMessage, + ProtocolErrorMessage, +} from "./types"; + +export default class Protocol { + private _disposeMessageListener: UnsubscribeFunction; + + constructor( + private type: string, + private handleMessage: (message: ProtocolRequestMessage) => any, + private protocol: IFrameProtocol + ) { + this._disposeMessageListener = this.protocol.channelListen( + async (msg: any) => { + if (msg.type === this.getTypeId() && msg.method) { + const message = msg as ProtocolRequestMessage; + try { + const result = await this.handleMessage(message); + const response: ProtocolResultMessage = { + type: this.getTypeId(), + msgId: message.msgId, + result: result, + }; + this.protocol.dispatch(response as any); + } catch (err: any) { + const response: ProtocolErrorMessage = { + type: this.getTypeId(), + msgId: message.msgId, + error: { + message: err.message, + }, + }; + this.protocol.dispatch(response as any); + } + } + } + ); + } + + getTypeId() { + return `protocol-${this.type}`; + } + + public dispose() { + this._disposeMessageListener(); + } +} diff --git a/sandpack-environments/src/clients/bundler/iframe-protocol.ts b/sandpack-environments/src/clients/bundler/iframe-protocol.ts new file mode 100644 index 000000000..74691f312 --- /dev/null +++ b/sandpack-environments/src/clients/bundler/iframe-protocol.ts @@ -0,0 +1,135 @@ +import type { + ListenerFunction, + SandpackMessage, + UnsubscribeFunction, +} from "./types"; + +export class IFrameProtocol { + private get frameWindow() { + return this.iframe.contentWindow; + } + private origin: string; + + // React to messages from any iframe + private globalListeners: Record = {}; + private globalListenersCount = 0; + + // React to messages from the iframe owned by this instance + public channelListeners: Record = {}; + private channelListenersCount = 0; + + // Random number to identify this instance of the client when messages are coming from multiple iframes + readonly channelId: number = Math.floor(Math.random() * 1000000); + + constructor(private iframe: HTMLIFrameElement, origin: string) { + this.origin = origin; + this.globalListeners = []; + this.channelListeners = []; + + this.eventListener = this.eventListener.bind(this); + + if (typeof window !== "undefined") { + window.addEventListener("message", this.eventListener); + } + } + + cleanup(): void { + window.removeEventListener("message", this.eventListener); + this.globalListeners = {}; + this.channelListeners = {}; + this.globalListenersCount = 0; + this.channelListenersCount = 0; + } + + // Sends the channelId and triggers an iframeHandshake promise to resolve, + // so the iframe can start listening for messages (based on the id) + register(): void { + if (!this.frameWindow) { + return; + } + + this.frameWindow.postMessage( + { + type: "register-frame", + origin: document.location.origin, + id: this.channelId, + }, + this.origin + ); + } + + // Messages are dispatched from the client directly to the instance iframe + dispatch(message: SandpackMessage): void { + if (!this.frameWindow) { + return; + } + + this.frameWindow.postMessage( + { + $id: this.channelId, + codesandbox: true, + ...message, + }, + this.origin + ); + } + + // Add a listener that is called on any message coming from an iframe in the page + // This is needed for the `initialize` message which comes without a channelId + globalListen(listener: ListenerFunction): UnsubscribeFunction { + if (typeof listener !== "function") { + return (): void => { + return; + }; + } + + const listenerId = this.globalListenersCount; + this.globalListeners[listenerId] = listener; + this.globalListenersCount++; + return (): void => { + delete this.globalListeners[listenerId]; + }; + } + + // Add a listener that is called on any message coming from an iframe with the instance channelId + // All other messages (eg: from other iframes) are ignored + channelListen(listener: ListenerFunction): UnsubscribeFunction { + if (typeof listener !== "function") { + return (): void => { + return; + }; + } + + const listenerId = this.channelListenersCount; + this.channelListeners[listenerId] = listener; + this.channelListenersCount++; + return (): void => { + delete this.channelListeners[listenerId]; + }; + } + + // Handles message windows coming from iframes + private eventListener(evt: MessageEvent): void { + // skip events originating from different iframes + if (evt.source !== this.frameWindow) { + return; + } + + const message = evt.data; + if (!message.codesandbox) { + return; + } + + Object.values(this.globalListeners).forEach((listener) => + listener(message) + ); + + if (message.$id !== this.channelId) { + return; + } + + Object.values(this.channelListeners).forEach((listener) => + listener(message) + ); + } +} diff --git a/sandpack-environments/src/clients/bundler/index.ts b/sandpack-environments/src/clients/bundler/index.ts new file mode 100644 index 000000000..3ae7447c2 --- /dev/null +++ b/sandpack-environments/src/clients/bundler/index.ts @@ -0,0 +1,131 @@ +import { InMemoryFileSystem } from "../../InMemoryFileSystem.js"; +import type { + SandpackEnvironment, + SandpackEnvironmentStatus, + SandpackPreview, + SandpackPreviewMessage, + SandpackPreviewStatus, +} from "../../types"; +import { debounce } from "../../utils"; + +import { SandpackBundlerPreview } from "./BundlerPreview"; +import type { + FileResolver, + NpmRegistry, + ReactDevToolsMode, + SandpackLogLevel, + BundlerType, +} from "./types.js"; + +export { BundlerType } from "./types"; + +export interface SandpackBundlerEnvironmentOptions { + bundlerType: BundlerType; + teamId?: string; + experimental_enableServiceWorker?: boolean; + experimental_stableServiceWorkerId?: string; + bundlerURL?: string; + externalResources?: string[]; + fileResolver?: FileResolver; + startRoute?: string; + autorun?: boolean; + autoReload?: boolean; + recompileMode?: "immediate" | "delayed"; + recompileDelay?: number; + id?: string; + bundlerTimeOut?: number; + entry?: string; + reactDevTools?: ReactDevToolsMode; + disableDependencyPreprocessing?: boolean; + showOpenInCodeSandbox?: boolean; + showErrorScreen?: boolean; + showLoadingScreen?: boolean; + skipEval?: boolean; + clearConsoleOnFirstCompile?: boolean; + logLevel?: SandpackLogLevel; + customNpmRegistries?: NpmRegistry[]; + sandboxId?: string; +} + +export class SandpackBundlerEnvironment implements SandpackEnvironment { + readonly type = "bundler"; + private previews = new Map(); + private statusSubscribers = new Set< + (status: SandpackEnvironmentStatus) => void + >(); + status: SandpackEnvironmentStatus = { + current: "LOADING", + progress: [], + }; + fs = new InMemoryFileSystem(); + + constructor(private options: SandpackBundlerEnvironmentOptions) { + this.fs.watch( + debounce(() => { + this.previews.forEach((preview) => { + preview.updateSandbox(); + }); + }, 10) + ); + } + + restart() { + // TODO + } + + onStatusChange( + callback: (status: SandpackEnvironmentStatus) => void + ): () => void { + this.statusSubscribers.add(callback); + return () => { + this.statusSubscribers.delete(callback); + }; + } + + createPreview(): SandpackPreview { + const iframe = document.createElement("iframe"); + const staticPreview = new SandpackBundlerPreview( + iframe, + this.options, + this.fs + ); + + this.previews.set(iframe, staticPreview); + + return { + iframe, + get status() { + return staticPreview.status; + }, + onStatusChange(subscriber: (status: SandpackPreviewStatus) => void) { + return staticPreview.onStatusChange(subscriber); + }, + onMessage(subscriber: (message: SandpackPreviewMessage) => void) { + return () => { + // Coming soon + }; + }, + back() { + // TODO + }, + forward() { + // TODO + }, + refresh() { + // TODO + }, + dispose: () => { + this.previews.delete(iframe); + staticPreview.dispose(); + }, + }; + } + + public dispose() { + this.fs.dispose(); + this.previews.forEach((preview) => { + preview.dispose(); + }); + this.previews.clear(); + } +} diff --git a/sandpack-environments/src/clients/bundler/mime.ts b/sandpack-environments/src/clients/bundler/mime.ts new file mode 100644 index 000000000..b0384bab7 --- /dev/null +++ b/sandpack-environments/src/clients/bundler/mime.ts @@ -0,0 +1,22 @@ +import mimeDB from "mime-db"; + +const extensionMap = new Map(); +const entries = Object.entries(mimeDB); +for (const [mimetype, entry] of entries) { + // eslint-disable-next-line + // @ts-ignore + if (!entry.extensions) { + continue; + } + + // eslint-disable-next-line + // @ts-ignore + const extensions = entry.extensions as string[]; + if (extensions.length) { + for (const ext of extensions) { + extensionMap.set(ext, mimetype); + } + } +} + +export const EXTENSIONS_MAP = extensionMap; diff --git a/sandpack-environments/src/clients/bundler/types.ts b/sandpack-environments/src/clients/bundler/types.ts new file mode 100644 index 000000000..abf003a6d --- /dev/null +++ b/sandpack-environments/src/clients/bundler/types.ts @@ -0,0 +1,433 @@ +export interface SandpackError { + message: string; + line?: number; + column?: number; + path?: string; + title?: string; +} + +export interface Module { + code: string; + path: string; +} + +export type Modules = Record< + string, + { + code: string; + path: string; + } +>; + +export interface ModuleSource { + fileName: string; + compiledCode: string; + sourceMap: unknown | undefined; +} + +export interface TranspiledModule { + module: Module; + query: string; + source: ModuleSource | undefined; + assets: Record; + isEntry: boolean; + isTestFile: boolean; + childModules: string[]; + /** + * All extra modules emitted by the loader + */ + emittedAssets: ModuleSource[]; + initiators: string[]; + dependencies: string[]; + asyncDependencies: string[]; + transpilationDependencies: string[]; + transpilationInitiators: string[]; +} + +export interface BundlerState { + entry: string; + transpiledModules: Record; +} + +export type ClientStatus = + | "initializing" + | "installing-dependencies" + | "transpiling" + | "evaluating" + | "running-tests" + | "idle" + | "done"; + +export interface ErrorStackFrame { + columnNumber: number; + fileName: string; + functionName: string; + lineNumber: number; + _originalColumnNumber: number; + _originalFileName: string; + _originalFunctionName: string; + _originalLineNumber: number; + _originalScriptCode: Array<{ + lineNumber: number; + content: string; + highlight: boolean; + }>; +} + +export interface SandpackErrorMessage { + title: string; + path: string; + message: string; + line: number; + column: number; + payload: { + frames?: ErrorStackFrame[]; + }; +} + +export interface NpmRegistry { + enabledScopes: string[]; + limitToScopes: boolean; + registryUrl: string; + /** + * It must be `false` if you're providing a sef-host solution, + * otherwise, it'll try to proxy from CodeSandbox Proxy + */ + proxyEnabled: boolean; + registryAuthToken?: string; +} + +export enum SandpackLogLevel { + None = 0, + Error = 10, + Warning = 20, + Info = 30, + Debug = 40, +} + +export type ReactDevToolsMode = "latest" | "legacy"; + +export type BundlerType = + | "angular-cli" + | "create-react-app" + | "create-react-app-typescript" + | "svelte" + | "parcel" + | "vue-cli" + | "static" + | "solid" + | "nextjs"; + +export type SandpackMessageConsoleMethods = + | "log" + | "debug" + | "info" + | "warn" + | "error" + | "table" + | "clear" + | "time" + | "timeEnd" + | "count" + | "assert"; + +type TestStatus = "running" | "pass" | "fail"; + +export type TestError = Error & { + matcherResult?: boolean; + mappedErrors?: Array<{ + fileName: string; + _originalFunctionName: string; + _originalColumnNumber: number; + _originalLineNumber: number; + _originalScriptCode: Array<{ + lineNumber: number; + content: string; + highlight: boolean; + }> | null; + }>; +}; + +export interface Test { + name: string; + blocks: string[]; + status: TestStatus; + path: string; + errors: TestError[]; + duration?: number | undefined; +} + +export type ListenerFunction = (msg: SandpackMessage) => void; +export type UnsubscribeFunction = () => void; + +export interface FileResolver { + isFile: (path: string) => Promise; + readFile: (path: string) => Promise; +} + +export interface BaseProtocolMessage { + type: string; + msgId: string; +} + +export interface ProtocolErrorMessage extends BaseProtocolMessage { + error: { + message: string; + }; +} + +export interface ProtocolResultMessage extends BaseProtocolMessage { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result: any; +} + +export interface ProtocolRequestMessage extends BaseProtocolMessage { + method: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params: any[]; +} + +export type SandboxTestMessage = + | RunAllTests + | RunTests + | ClearJestErrors + | ({ type: "test" } & ( + | InitializedTestsMessage + | TestCountMessage + | TotalTestStartMessage + | TotalTestEndMessage + | AddFileMessage + | RemoveFileMessage + | FileErrorMessage + | DescribeStartMessage + | DescribeEndMessage + | AddTestMessage + | TestStartMessage + | TestEndMessage + )); + +interface InitializedTestsMessage { + event: "initialize_tests"; +} + +interface ClearJestErrors { + type: "action"; + action: "clear-errors"; + source: "jest"; + path: string; +} + +interface TestCountMessage { + event: "test_count"; + count: number; +} + +interface TotalTestStartMessage { + event: "total_test_start"; +} + +interface TotalTestEndMessage { + event: "total_test_end"; +} + +interface AddFileMessage { + event: "add_file"; + path: string; +} + +interface RemoveFileMessage { + event: "remove_file"; + path: string; +} + +interface FileErrorMessage { + event: "file_error"; + path: string; + error: TestError; +} + +interface DescribeStartMessage { + event: "describe_start"; + blockName: string; +} + +interface DescribeEndMessage { + event: "describe_end"; +} + +interface AddTestMessage { + event: "add_test"; + testName: string; + path: string; +} + +interface TestStartMessage { + event: "test_start"; + test: Test; +} + +interface TestEndMessage { + event: "test_end"; + test: Test; +} + +interface RunAllTests { + type: "run-all-tests"; +} + +interface RunTests { + type: "run-tests"; + path: string; +} + +export interface BaseSandpackMessage { + type: string; + $id?: number; + codesandbox?: boolean; +} + +export type SandpackMessage = BaseSandpackMessage & + ( + | { + type: "initialized"; + } + | { + type: "start"; + firstLoad?: boolean; + } + | { + type: "status"; + status: ClientStatus; + } + | { + type: "state"; + state: BundlerState; + } + | { + type: "success"; + } + | ({ + type: "action"; + action: "show-error"; + } & SandpackErrorMessage) + | { + type: "action"; + action: "notification"; + notificationType: "error"; + title: string; + } + | { + type: "done"; + compilatonError: boolean; + } + | { + type: "urlchange"; + url: string; + back: boolean; + forward: boolean; + } + | { + type: "resize"; + height: number; + } + | { + type: "transpiler-context"; + data: Record>; + } + | { + type: "compile"; + version: number; + isInitializationCompile?: boolean; + modules: Modules; + externalResources: string[]; + hasFileResolver: boolean; + disableDependencyPreprocessing?: boolean; + experimental_enableServiceWorker?: boolean; + experimental_stableServiceWorkerId?: string; + template?: string | BundlerType; + showOpenInCodeSandbox: boolean; + showErrorScreen: boolean; + showLoadingScreen: boolean; + skipEval: boolean; + clearConsoleDisabled?: boolean; + reactDevTools?: ReactDevToolsMode; + logLevel?: SandpackLogLevel; + customNpmRegistries?: NpmRegistry[]; + teamId?: string; + sandboxId?: string; + } + | { + type: "refresh"; + } + | { + type: "urlback"; + } + | { + type: "urlforward"; + } + | { + type: "get-transpiler-context"; + } + | { type: "get-modules" } + | { type: "all-modules"; data: Array<{ path: string; code: string }> } + | { + type: "activate-react-devtools"; + } + | { + type: "console"; + log: Array<{ + method: SandpackMessageConsoleMethods; + id: string; + data: string[]; + }>; + } + | SandboxTestMessage + | { type: "sign-in"; teamId: string } + | { type: "sign-out" } + | { + type: "dependencies"; + data: + | { + state: "downloading_manifest"; + } + | { + state: "downloaded_module"; + name: string; + total: number; + progress: number; + } + | { + state: "starting"; + }; + } + ); + +export interface SandpackBundlerFile { + code: string; + hidden?: boolean; + active?: boolean; + readOnly?: boolean; +} + +export type SandpackBundlerFiles = Record; + +export type Dependencies = Record; + +export const CHANNEL_NAME = "$CSB_RELAY"; + +export interface IPreviewRequestMessage { + $channel: typeof CHANNEL_NAME; + $type: "preview/request"; + id: string; + method: string; + url: string; +} + +export interface IPreviewResponseMessage { + $channel: typeof CHANNEL_NAME; + $type: "preview/response"; + id: string; + status: number; + headers: Record; + body: string | Uint8Array; +} diff --git a/sandpack-environments/src/clients/bundler/utils.ts b/sandpack-environments/src/clients/bundler/utils.ts new file mode 100644 index 000000000..8cf051b31 --- /dev/null +++ b/sandpack-environments/src/clients/bundler/utils.ts @@ -0,0 +1,392 @@ +import { invariant } from "outvariant"; + +import type { SandpackFileSystem } from "../../types"; + +import type { + SandpackBundlerFiles, + Dependencies, + SandpackErrorMessage, + SandpackError, + ErrorStackFrame, +} from "./types"; + +export const createError = (message: string): string => + `[sandpack-client]: ${message}`; + +export function nullthrows(value?: T | null, err = "Value is nullish"): T { + invariant(value != null, createError(err)); + + return value; +} + +const DEPENDENCY_ERROR_MESSAGE = `"dependencies" was not specified - provide either a package.json or a "dependencies" value`; +const ENTRY_ERROR_MESSAGE = `"entry" was not specified - provide either a package.json with the "main" field or an "entry" value`; + +export function createPackageJSON(entry = "/index.js"): string { + return JSON.stringify( + { + name: "sandpack-project", + main: entry, + dependencies: {}, + devDependencies: {}, + }, + null, + 2 + ); +} + +export function ensureValidBundlerFiles( + files: SandpackBundlerFiles, + entry?: string +): SandpackBundlerFiles { + const normalizedFiles = normalizePaths(files); + + const packageJsonFile = normalizedFiles["/package.json"]; + + if (!packageJsonFile) { + normalizedFiles["/package.json"] = { + code: createPackageJSON(entry), + }; + } + + return normalizedFiles; +} + +export function extractErrorDetails(msg: SandpackErrorMessage): SandpackError { + if (msg.title === "SyntaxError") { + const { title, path, message, line, column } = msg; + return { title, path, message, line, column }; + } + + const relevantStackFrame = getRelevantStackFrame(msg.payload?.frames); + if (!relevantStackFrame) { + return { message: msg.message }; + } + + const errorInCode = getErrorInOriginalCode(relevantStackFrame); + const errorLocation = getErrorLocation(relevantStackFrame); + const errorMessage = formatErrorMessage( + relevantStackFrame._originalFileName, + msg.message, + errorLocation, + errorInCode + ); + + return { + message: errorMessage, + title: msg.title, + path: relevantStackFrame._originalFileName, + line: relevantStackFrame._originalLineNumber, + column: relevantStackFrame._originalColumnNumber, + }; +} + +function getRelevantStackFrame( + frames?: ErrorStackFrame[] +): ErrorStackFrame | undefined { + if (!frames) { + return; + } + + return frames.find((frame) => !!frame._originalFileName); +} + +function getErrorLocation(errorFrame: ErrorStackFrame): string { + return errorFrame + ? ` (${errorFrame._originalLineNumber}:${errorFrame._originalColumnNumber})` + : ``; +} + +function getErrorInOriginalCode(errorFrame: ErrorStackFrame): string { + const lastScriptLine = + errorFrame._originalScriptCode[errorFrame._originalScriptCode.length - 1]; + const numberOfLineNumberCharacters = + lastScriptLine.lineNumber.toString().length; + + const leadingCharacterOffset = 2; + const barSeparatorCharacterOffset = 3; + const extraLineLeadingSpaces = + leadingCharacterOffset + + numberOfLineNumberCharacters + + barSeparatorCharacterOffset + + errorFrame._originalColumnNumber; + + return errorFrame._originalScriptCode.reduce((result, scriptLine) => { + const leadingChar = scriptLine.highlight ? ">" : " "; + const lineNumber = + scriptLine.lineNumber.toString().length === numberOfLineNumberCharacters + ? `${scriptLine.lineNumber}` + : ` ${scriptLine.lineNumber}`; + + const extraLine = scriptLine.highlight + ? "\n" + " ".repeat(extraLineLeadingSpaces) + "^" + : ""; + + return ( + result + // accumulator + "\n" + + leadingChar + // > or " " + " " + + lineNumber + // line number on equal number of characters + " | " + + scriptLine.content + // code + extraLine // line under the highlighed line to show the column index + ); + }, ""); +} + +function formatErrorMessage( + filePath: string, + message: string, + location: string, + errorInCode: string +): string { + return `${filePath}: ${message}${location} +${errorInCode}`; +} + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const normalizePaths = (path: R): R => { + if (typeof path === "string") { + return (path.startsWith("/") ? path : `/${path}`) as R; + } + + if (Array.isArray(path)) { + return path.map((p) => (p.startsWith("/") ? p : `/${p}`)) as R; + } + + if (typeof path === "object" && path !== null) { + return Object.entries(path as any).reduce( + (acc, [key, content]: [string, string | any]) => { + const fileName = key.startsWith("/") ? key : `/${key}`; + + acc[fileName] = content; + + return acc; + }, + {} + ); + } + + return null as R; +}; +const MAX_CLIENT_DEPENDENCY_COUNT = 50; + +interface PackageJSON { + dependencies?: Dependencies; + devDependencies?: Dependencies; +} + +export function getTemplate( + pkg: PackageJSON | null, + /* eslint-disable @typescript-eslint/no-explicit-any */ + modules: any +): string | undefined { + if (!pkg) { + return "static"; + } + + const { dependencies = {}, devDependencies = {} } = pkg; + + const totalDependencies = [ + ...Object.keys(dependencies), + ...Object.keys(devDependencies), + ]; + const moduleNames = Object.keys(modules); + + const adonis = ["@adonisjs/framework", "@adonisjs/core"]; + + if (totalDependencies.some((dep) => adonis.indexOf(dep) > -1)) { + return "adonis"; + } + + const nuxt = ["nuxt", "nuxt-edge", "nuxt-ts", "nuxt-ts-edge", "nuxt3"]; + + if (totalDependencies.some((dep) => nuxt.indexOf(dep) > -1)) { + return "nuxt"; + } + + if (totalDependencies.indexOf("next") > -1) { + return "next"; + } + + const apollo = [ + "apollo-server", + "apollo-server-express", + "apollo-server-hapi", + "apollo-server-koa", + "apollo-server-lambda", + "apollo-server-micro", + ]; + + if (totalDependencies.some((dep) => apollo.indexOf(dep) > -1)) { + return "apollo"; + } + + if (totalDependencies.indexOf("mdx-deck") > -1) { + return "mdx-deck"; + } + + if (totalDependencies.indexOf("gridsome") > -1) { + return "gridsome"; + } + + if (totalDependencies.indexOf("vuepress") > -1) { + return "vuepress"; + } + + if (totalDependencies.indexOf("ember-cli") > -1) { + return "ember"; + } + + if (totalDependencies.indexOf("sapper") > -1) { + return "sapper"; + } + + if (totalDependencies.indexOf("gatsby") > -1) { + return "gatsby"; + } + + if (totalDependencies.indexOf("quasar") > -1) { + return "quasar"; + } + + if (totalDependencies.indexOf("@docusaurus/core") > -1) { + return "docusaurus"; + } + + if (totalDependencies.indexOf("remix") > -1) { + return "remix"; + } + + if (totalDependencies.indexOf("astro") > -1) { + return "node"; + } + + // CLIENT + + if (moduleNames.some((m) => m.endsWith(".re"))) { + return "reason"; + } + + const parcel = ["parcel-bundler", "parcel"]; + if (totalDependencies.some((dep) => parcel.indexOf(dep) > -1)) { + return "parcel"; + } + + const dojo = ["@dojo/core", "@dojo/framework"]; + if (totalDependencies.some((dep) => dojo.indexOf(dep) > -1)) { + return "@dojo/cli-create-app"; + } + if ( + totalDependencies.indexOf("@nestjs/core") > -1 || + totalDependencies.indexOf("@nestjs/common") > -1 + ) { + return "nest"; + } + + if (totalDependencies.indexOf("react-styleguidist") > -1) { + return "styleguidist"; + } + + if (totalDependencies.indexOf("react-scripts") > -1) { + return "create-react-app"; + } + + if (totalDependencies.indexOf("react-scripts-ts") > -1) { + return "create-react-app-typescript"; + } + + if (totalDependencies.indexOf("@angular/core") > -1) { + return "angular-cli"; + } + + if (totalDependencies.indexOf("preact-cli") > -1) { + return "preact-cli"; + } + + if ( + totalDependencies.indexOf("@sveltech/routify") > -1 || + totalDependencies.indexOf("@roxi/routify") > -1 + ) { + return "node"; + } + + if (totalDependencies.indexOf("vite") > -1) { + return "node"; + } + + if (totalDependencies.indexOf("@frontity/core") > -1) { + return "node"; + } + + if (totalDependencies.indexOf("svelte") > -1) { + return "svelte"; + } + + if (totalDependencies.indexOf("vue") > -1) { + return "vue-cli"; + } + + if (totalDependencies.indexOf("cx") > -1) { + return "cxjs"; + } + + const nodeDeps = [ + "express", + "koa", + "nodemon", + "ts-node", + "@tensorflow/tfjs-node", + "webpack-dev-server", + "snowpack", + ]; + if (totalDependencies.some((dep) => nodeDeps.indexOf(dep) > -1)) { + return "node"; + } + + if (Object.keys(dependencies).length >= MAX_CLIENT_DEPENDENCY_COUNT) { + // The dependencies are too much for client sandboxes to handle + return "node"; + } + + return undefined; +} + +export function getExtension(filepath: string): string { + const parts = filepath.split("."); + if (parts.length <= 1) { + return ""; + } else { + const ext = parts[parts.length - 1]; + return ext; + } +} + +export async function readAllFiles( + fs: SandpackFileSystem, + directoryPath: string, + files: SandpackBundlerFiles +) { + const entries = await fs.readDirectory(directoryPath); + + await Promise.all( + entries.map(async (entry) => { + const fullPath = + directoryPath === "/" + ? directoryPath + entry.name + : directoryPath + "/" + entry.name; + if (entry.type === "directory") { + return readAllFiles(fs, fullPath, files); + } + + const code = await fs.readFile(fullPath); + + if (typeof code === "string") { + files[fullPath] = { code }; + } + }) + ); + + return files; +} diff --git a/sandpack-environments/src/clients/static/StaticPreview.ts b/sandpack-environments/src/clients/static/StaticPreview.ts new file mode 100644 index 000000000..23dd96332 --- /dev/null +++ b/sandpack-environments/src/clients/static/StaticPreview.ts @@ -0,0 +1,240 @@ +import type { FileContent } from "static-browser-server"; +import { PreviewController } from "static-browser-server"; + +import type { InMemoryFileSystem } from "../../InMemoryFileSystem.js"; +// @ts-expect-error // get the bundled file, which contains all dependencies +import consoleHook from "../../inject-scripts/dist/consoleHook.js"; +import type { SandpackBundlerMessages } from "../../sandpack-bundler-types"; +import type { + SandpackPreviewMessage, + SandpackPreviewStatus, +} from "../../types"; + +import { + generateRandomId, + insertHtmlAfterRegex, + readBuffer, + validateHtml, +} from "./utils"; + +import type { SandpackStaticEnvironmentOptions } from "./index.js"; + +export class SandpackStaticPreview { + private disposers = new Set<() => void>(); + private iframeMessageSubscribers = new Set< + (message: SandpackPreviewMessage) => void + >(); + private statusSubscribers = new Set< + (status: SandpackPreviewStatus) => void + >(); + private previewController: PreviewController; + private iframe: HTMLIFrameElement; + private _status: SandpackPreviewStatus = { + current: "READY", + }; + get status() { + return this._status; + } + set status(newStatus) { + this._status = newStatus; + this.statusSubscribers.forEach((subscriber) => { + subscriber(newStatus); + }); + } + constructor( + iframe: HTMLIFrameElement, + private options: SandpackStaticEnvironmentOptions, + private fs: InMemoryFileSystem + ) { + this.previewController = this.createPreviewController(); + this.iframe = this.configureIframe(iframe); + } + // Handles message windows coming from iframes + private onIframeMessage(evt: MessageEvent): void { + const message = evt.data; + if (!message.codesandbox) { + return; + } + + // TODO: Dispatch urlchange? + // this.dispatch(message); + + this.iframeMessageSubscribers.forEach((subscriber) => { + subscriber(message); + }); + } + + private configureIframe(iframe: HTMLIFrameElement) { + if (!iframe.getAttribute("sandbox")) { + iframe.setAttribute( + "sandbox", + "allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts allow-downloads allow-pointer-lock" + ); + + iframe.setAttribute( + "allow", + "accelerometer; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; clipboard-read; clipboard-write; xr-spatial-tracking;" + ); + } + + if (typeof window !== "undefined") { + const listener = (evt: MessageEvent) => { + // skip events originating from different iframes + if (evt.source !== this.iframe.contentWindow) { + return; + } + this.onIframeMessage(evt); + }; + + window.addEventListener("message", listener); + + this.disposers.add(() => { + window.removeEventListener("message", listener); + }); + } + + return iframe; + } + + private createPreviewController() { + return new PreviewController({ + baseUrl: + this.options.bundlerURL ?? + "https://preview.sandpack-static-server.codesandbox.io", + // filepath is always normalized to start with / and not end with a slash + getFileContent: async (filepath) => { + let content = await this.fs.readFile(filepath); + + if (!content) { + throw new Error("File not found"); + } + if (filepath.endsWith(".html") || filepath.endsWith(".htm")) { + try { + content = validateHtml(content); + content = this.injectProtocolScript(content); + content = this.injectExternalResources( + content, + this.options.externalResources + ); + content = this.injectScriptIntoHead(content, { + script: consoleHook, + scope: { channelId: generateRandomId() }, + }); + } catch (err) { + console.error("Runtime injection failed", err); + } + } + return content; + }, + }); + } + + private injectContentIntoHead( + content: FileContent, + contentToInsert: string + ): FileContent { + // Make it a string + content = readBuffer(content); + + // Inject script + content = + insertHtmlAfterRegex(/]*>/g, content, "\n" + contentToInsert) ?? + contentToInsert + "\n" + content; + + return content; + } + + private injectProtocolScript(content: FileContent): FileContent { + const scriptToInsert = ``; + + return this.injectContentIntoHead(content, scriptToInsert); + } + + private injectExternalResources( + content: FileContent, + externalResources: string[] = [] + ): FileContent { + const tagsToInsert = externalResources + .map((resource) => { + const match = resource.match(/\.([^.]*)$/); + const fileType = match?.[1]; + + if (fileType === "css" || resource.includes("fonts.googleapis")) { + return ``; + } + + if (fileType === "js") { + return ``; + } + + throw new Error( + `Unable to determine file type for external resource: ${resource}` + ); + }) + .join("\n"); + + return this.injectContentIntoHead(content, tagsToInsert); + } + + private injectScriptIntoHead( + content: FileContent, + opts: { + script: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + scope?: { channelId: string } & Record; + } + ): FileContent { + const { script, scope = {} } = opts; + const scriptToInsert = ` + + `.trim(); + + return this.injectContentIntoHead(content, scriptToInsert); + } + + onMessage(callback: (message: SandpackPreviewMessage) => void): () => void { + this.iframeMessageSubscribers.add(callback); + return () => { + this.iframeMessageSubscribers.delete(callback); + }; + } + + onStatusChange( + callback: (status: SandpackPreviewStatus) => void + ): () => void { + this.statusSubscribers.add(callback); + return () => { + this.statusSubscribers.delete(callback); + }; + } + + async compile(): Promise { + const previewUrl = await this.previewController.initPreview(); + this.iframe.setAttribute("src", previewUrl); + + this.sendBundlerMessage({ type: "done", compilatonError: false }); + this.sendBundlerMessage({ + type: "urlchange", + url: previewUrl, + back: false, + forward: false, + }); + } + + sendBundlerMessage(message: SandpackBundlerMessages): void { + this.iframe.contentWindow?.postMessage(message, "*"); + } + + dispose(): void { + // TODO: Dispose of the preview + } +} diff --git a/sandpack-environments/src/clients/static/index.ts b/sandpack-environments/src/clients/static/index.ts new file mode 100644 index 000000000..e1fc935ed --- /dev/null +++ b/sandpack-environments/src/clients/static/index.ts @@ -0,0 +1,97 @@ +import { InMemoryFileSystem } from "../../InMemoryFileSystem.js"; +import type { + SandpackEnvironment, + SandpackEnvironmentStatus, + SandpackPreview, + SandpackPreviewMessage, + SandpackPreviewStatus, +} from "../../types"; +import { debounce } from "../../utils"; + +import { SandpackStaticPreview } from "./StaticPreview.js"; + +export interface SandpackStaticEnvironmentOptions { + bundlerURL?: string; + externalResources?: string[]; +} + +export class SandpackStaticEnvironment implements SandpackEnvironment { + readonly type = "static"; + private previews = new Map(); + private statusSubscribers = new Set< + (status: SandpackEnvironmentStatus) => void + >(); + status: SandpackEnvironmentStatus = { + current: "LOADING", + progress: [], + }; + fs = new InMemoryFileSystem(); + + constructor(private options: SandpackStaticEnvironmentOptions) { + this.fs.watch( + debounce(() => { + this.previews.forEach((preview) => { + preview.compile(); + }); + }, 10) + ); + } + + restart() { + // TODO + } + + onStatusChange( + callback: (status: SandpackEnvironmentStatus) => void + ): () => void { + this.statusSubscribers.add(callback); + return () => { + this.statusSubscribers.delete(callback); + }; + } + + createPreview(): SandpackPreview { + const iframe = document.createElement("iframe"); + const staticPreview = new SandpackStaticPreview( + iframe, + this.options, + this.fs + ); + + this.previews.set(iframe, staticPreview); + + return { + iframe, + get status() { + return staticPreview.status; + }, + onStatusChange(subscriber: (status: SandpackPreviewStatus) => void) { + return staticPreview.onStatusChange(subscriber); + }, + onMessage(subscriber: (message: SandpackPreviewMessage) => void) { + return staticPreview.onMessage(subscriber); + }, + back() { + // TODO + }, + forward() { + // TODO + }, + refresh() { + // TODO + }, + dispose: () => { + this.previews.delete(iframe); + staticPreview.dispose(); + }, + }; + } + + public dispose() { + this.fs.dispose(); + this.previews.forEach((preview) => { + preview.dispose(); + }); + this.previews.clear(); + } +} diff --git a/sandpack-environments/src/clients/static/utils.ts b/sandpack-environments/src/clients/static/utils.ts new file mode 100644 index 000000000..be93d3a24 --- /dev/null +++ b/sandpack-environments/src/clients/static/utils.ts @@ -0,0 +1,48 @@ +import type { FileContent } from "static-browser-server"; + +export const insertHtmlAfterRegex = ( + regex: RegExp, + content: string, + insertable: string +) => { + const match = regex.exec(content); + if (match && match.length >= 1) { + const offset = match.index + match[0].length; + const prefix = content.substring(0, offset); + const suffix = content.substring(offset); + return prefix + insertable + suffix; + } +}; + +export const readBuffer = (content: string | Uint8Array): string => { + if (typeof content === "string") { + return content; + } else { + return new TextDecoder().decode(content); + } +}; + +export const validateHtml = (content: FileContent): FileContent => { + // Make it a string + const contentString = readBuffer(content); + + const domParser = new DOMParser(); + const doc = domParser.parseFromString(contentString, "text/html"); + + if (!doc.documentElement.getAttribute("lang")) { + doc.documentElement.setAttribute("lang", "en"); + } + + const html = doc.documentElement.outerHTML; + + return `\n${html}`; +}; + +let counter = 0; + +export function generateRandomId() { + const now = Date.now(); + const randomNumber = Math.round(Math.random() * 10000); + const count = (counter += 1); + return (+`${now}${randomNumber}${count}`).toString(16); +} diff --git a/sandpack-environments/src/clients/vm/VMFileSystem.ts b/sandpack-environments/src/clients/vm/VMFileSystem.ts new file mode 100644 index 000000000..2278bed94 --- /dev/null +++ b/sandpack-environments/src/clients/vm/VMFileSystem.ts @@ -0,0 +1,77 @@ +import type { SandboxSession } from "@codesandbox/sdk"; + +import type { DirectoryEntry, SandpackFileSystem } from "../../types"; + +export class VMFileSystem implements SandpackFileSystem { + constructor( + private sandboxPromise: Promise, + private workspacePath: string + ) {} + + async writeFile(path: string, content: string): Promise { + const sandbox = await this.sandboxPromise; + + await sandbox.fs.writeTextFile(this.workspacePath + path, content, { + create: true, + overwrite: true, + }); + } + + writeFileMetadata(path: string, metadata: object): void { + // Not needed in VMs + } + + readFileMetadata(path: string) { + return {}; + } + + async readFile(path: string): Promise { + const sandbox = await this.sandboxPromise; + const file = await sandbox.fs.readFile(this.workspacePath + path); + + return new TextDecoder().decode(file); + } + + async readDirectory(path: string): Promise { + const sandbox = await this.sandboxPromise; + const entries = await sandbox.fs.readdir(this.workspacePath + path); + + return entries; + } + + createDirectory(path: string): Promise { + throw new Error("Not implemented"); + } + + deleteFile(path: string): Promise { + throw new Error("Not implemented"); + } + + deleteDirectory(path: string): Promise { + throw new Error("Not implemented"); + } + + watchDirectory(path: string, callback: () => void): () => void { + const watcherPromise = this.sandboxPromise.then((sandbox) => + sandbox.fs.watch(this.workspacePath + path) + ); + + watcherPromise.then((watcher) => { + watcher.onEvent(callback); + }); + + return () => { + watcherPromise.then((watcher) => { + watcher.dispose(); + }); + }; + } + + watch(callback: () => void): () => void { + throw new Error("Not implemented"); + } + + dispose(): void { + // Clean up resources if needed + } +} diff --git a/sandpack-environments/src/clients/vm/VMPreview.ts b/sandpack-environments/src/clients/vm/VMPreview.ts new file mode 100644 index 000000000..c98696b27 --- /dev/null +++ b/sandpack-environments/src/clients/vm/VMPreview.ts @@ -0,0 +1,102 @@ +import type { SandpackBundlerMessages } from "../../sandpack-bundler-types"; +import type { + SandpackPreviewMessage, + SandpackPreviewStatus, +} from "../../types"; + +export class VMPreview { + private disposers = new Set<() => void>(); + private iframeMessageSubscribers = new Set< + (message: SandpackPreviewMessage) => void + >(); + private statusSubscribers = new Set< + (status: SandpackPreviewStatus) => void + >(); + private iframe: HTMLIFrameElement; + private _status: SandpackPreviewStatus = { + current: "READY", + }; + get status() { + return this._status; + } + set status(newStatus) { + this._status = newStatus; + this.statusSubscribers.forEach((subscriber) => { + subscriber(newStatus); + }); + } + constructor(iframe: HTMLIFrameElement) { + this.iframe = this.configureIframe(iframe); + this.iframe.src = "https://qc7lnq-5173.csb.app"; + } + // Handles message windows coming from iframes + private onIframeMessage(evt: MessageEvent): void { + const message = evt.data; + if (!message.codesandbox) { + return; + } + + // TODO: Dispatch urlchange? + // this.dispatch(message); + + this.iframeMessageSubscribers.forEach((subscriber) => { + subscriber(message); + }); + } + + private configureIframe(iframe: HTMLIFrameElement) { + if (!iframe.getAttribute("sandbox")) { + iframe.setAttribute( + "sandbox", + "allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts allow-downloads allow-pointer-lock" + ); + + iframe.setAttribute( + "allow", + "accelerometer; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; clipboard-read; clipboard-write; xr-spatial-tracking;" + ); + } + + if (typeof window !== "undefined") { + const listener = (evt: MessageEvent) => { + // skip events originating from different iframes + if (evt.source !== this.iframe.contentWindow) { + return; + } + this.onIframeMessage(evt); + }; + + window.addEventListener("message", listener); + + this.disposers.add(() => { + window.removeEventListener("message", listener); + }); + } + + return iframe; + } + + onMessage(callback: (message: SandpackPreviewMessage) => void): () => void { + this.iframeMessageSubscribers.add(callback); + return () => { + this.iframeMessageSubscribers.delete(callback); + }; + } + + onStatusChange( + callback: (status: SandpackPreviewStatus) => void + ): () => void { + this.statusSubscribers.add(callback); + return () => { + this.statusSubscribers.delete(callback); + }; + } + + sendBundlerMessage(message: SandpackBundlerMessages): void { + this.iframe.contentWindow?.postMessage(message, "*"); + } + + dispose(): void { + // TODO: Dispose of the preview + } +} diff --git a/sandpack-environments/src/clients/vm/index.ts b/sandpack-environments/src/clients/vm/index.ts new file mode 100644 index 000000000..f48a1f589 --- /dev/null +++ b/sandpack-environments/src/clients/vm/index.ts @@ -0,0 +1,98 @@ +import type { SessionData } from "@codesandbox/sdk"; +import { connectToSandbox } from "@codesandbox/sdk/browser"; + +import type { + SandpackEnvironment, + SandpackEnvironmentStatus, + SandpackPreview, + SandpackPreviewMessage, + SandpackPreviewStatus, +} from "../../types"; + +import { VMFileSystem } from "./VMFileSystem"; +import { VMPreview } from "./VMPreview"; + +export interface SandpackVMEnvironmentOptions { + session: SessionData; +} + +export class SandpackVMEnvironment implements SandpackEnvironment { + readonly type = "vm"; + private statusSubscribers = new Set< + (status: SandpackEnvironmentStatus) => void + >(); + fs: VMFileSystem; + private _status: SandpackEnvironmentStatus = { + current: "LOADING", + progress: [], + }; + get status() { + return this._status; + } + set status(newStatus) { + this._status = newStatus; + this.statusSubscribers.forEach((subscriber) => { + subscriber(newStatus); + }); + } + + constructor(private options: SandpackVMEnvironmentOptions) { + const sandboxPromise = connectToSandbox(options.session); + this.fs = new VMFileSystem( + sandboxPromise, + options.session.user_workspace_path + ); + sandboxPromise.then(() => { + this.status = { + current: "READY", + }; + }); + } + + restart() { + // TODO + } + + onStatusChange( + callback: (status: SandpackEnvironmentStatus) => void + ): () => void { + this.statusSubscribers.add(callback); + return () => { + this.statusSubscribers.delete(callback); + }; + } + + createPreview(): SandpackPreview { + const iframe = document.createElement("iframe"); + const vmPreview = new VMPreview(iframe); + + return { + iframe, + get status() { + return vmPreview.status; + }, + onStatusChange(subscriber: (status: SandpackPreviewStatus) => void) { + return vmPreview.onStatusChange(subscriber); + }, + onMessage(subscriber: (message: SandpackPreviewMessage) => void) { + return vmPreview.onMessage(subscriber); + }, + back() { + // TODO + }, + forward() { + // TODO + }, + refresh() { + // TODO + }, + dispose: () => { + vmPreview.dispose(); + }, + }; + } + + public dispose() { + // TODO + } +} diff --git a/sandpack-environments/src/index.ts b/sandpack-environments/src/index.ts new file mode 100644 index 000000000..08d6a2c71 --- /dev/null +++ b/sandpack-environments/src/index.ts @@ -0,0 +1,35 @@ +import type { SandpackEnvironmentOptions, SandpackEnvironment } from "./types"; + +export { + SandpackEnvironment, + DirectoryEntry, + SandpackPreview, + SandpackFileSystem, + SandpackEnvironmentOptions, +} from "./types"; + +export * from "./clients/bundler"; +export * from "./clients/static"; +export * from "./clients/vm"; + +export async function loadEnvironment( + options: T +): Promise { + switch (options.type) { + case "static": { + return import("./clients/static").then( + (m) => new m.SandpackStaticEnvironment(options) + ); + } + case "bundler": { + return import("./clients/bundler").then( + (m) => new m.SandpackBundlerEnvironment(options) + ); + } + case "vm": { + return import("./clients/vm").then( + (m) => new m.SandpackVMEnvironment(options) + ); + } + } +} diff --git a/sandpack-environments/src/inject-scripts/consoleHook.ts b/sandpack-environments/src/inject-scripts/consoleHook.ts new file mode 100644 index 000000000..2fc38d769 --- /dev/null +++ b/sandpack-environments/src/inject-scripts/consoleHook.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/ban-ts-comment,no-console,@typescript-eslint/explicit-function-return-type, no-restricted-globals */ +import Hook from "console-feed/lib/Hook"; +import { Encode } from "console-feed/lib/Transform"; + +declare global { + const scope: { channelId: string }; +} + +Hook(window.console, (log) => { + const encodeMessage = Encode(log) as any; + parent.postMessage( + { + type: "console", + codesandbox: true, + log: Array.isArray(encodeMessage) ? encodeMessage[0] : encodeMessage, + channelId: scope.channelId, + }, + "*" + ); +}); diff --git a/sandpack-environments/src/sandpack-bundler-types.ts b/sandpack-environments/src/sandpack-bundler-types.ts new file mode 100644 index 000000000..d8b721f69 --- /dev/null +++ b/sandpack-environments/src/sandpack-bundler-types.ts @@ -0,0 +1,79 @@ +export enum SandpackLogLevel { + None = 0, + Error = 10, + Warning = 20, + Info = 30, + Debug = 40, +} + +export interface ErrorStackFrame { + columnNumber: number; + fileName: string; + functionName: string; + lineNumber: number; + _originalColumnNumber: number; + _originalFileName: string; + _originalFunctionName: string; + _originalLineNumber: number; + _originalScriptCode: Array<{ + lineNumber: number; + content: string; + highlight: boolean; + }>; +} + +export interface SandpackBundlerErrorMessage { + title: string; + path: string; + message: string; + line: number; + column: number; + payload: { + frames?: ErrorStackFrame[]; + }; +} + +export type FileContent = Uint8Array | string; + +export type FilesMap = Record; + +export type SandpackBundlerMessages = + | { + type: "start"; + firstLoad?: boolean; + } + | { + type: "done"; + compilatonError: boolean; + } + | { + type: "compile"; + modules: FilesMap; + template?: string; + logLevel?: SandpackLogLevel; + } + | ({ + type: "action"; + action: "show-error"; + } & SandpackBundlerErrorMessage) + | { + type: "action"; + action: "notification"; + notificationType: "error"; + title: string; + } + | { + type: "urlchange"; + url: string; + back: boolean; + forward: boolean; + } + | { + type: "refresh"; + } + | { + type: "urlback"; + } + | { + type: "urlforward"; + }; diff --git a/sandpack-environments/src/types.ts b/sandpack-environments/src/types.ts new file mode 100644 index 000000000..df0609053 --- /dev/null +++ b/sandpack-environments/src/types.ts @@ -0,0 +1,91 @@ +import type { SandpackBundlerEnvironmentOptions } from "./clients/bundler"; +import type { SandpackStaticEnvironmentOptions } from "./clients/static"; +import type { SandpackVMEnvironmentOptions } from "./clients/vm"; +import type { FileContent } from "./sandpack-bundler-types"; + +export type SandpackEnvironmentOptions = + | ({ + type: "static"; + } & SandpackStaticEnvironmentOptions) + | ({ + type: "bundler"; + } & SandpackBundlerEnvironmentOptions) + | ({ + type: "vm"; + } & SandpackVMEnvironmentOptions); + +export interface DirectoryEntry { + name: string; + type: "file" | "directory"; +} + +export interface SandpackFileSystem { + writeFile(path: string, content: FileContent): Promise; + readFile(path: string): Promise; + writeFileMetadata(path: string, metadata: object): void; + readFileMetadata(path: string): object; + readDirectory(path: string): Promise; + createDirectory(path: string): Promise; + deleteFile(path: string): Promise; + deleteDirectory(path: string): Promise; + watchDirectory(path: string, callback: () => void): () => void; + watch(callback: () => void): () => void; + dispose(): void; +} + +export interface SandpackPreviewMessage { + type: "urlchange"; + url: string; + back: boolean; + forward: boolean; +} + +export type SandpackPreviewStatus = + | { + current: "LOADING"; + progress: string[]; + } + | { + current: "READY"; + } + | { + current: "ERROR"; + error: Error; + }; + +// TODO: Implement method to initialize iframe +export interface SandpackPreview { + status: SandpackPreviewStatus; + iframe: HTMLIFrameElement; + onStatusChange(listener: (status: SandpackPreviewStatus) => void): () => void; + onMessage(listener: (message: SandpackPreviewMessage) => void): () => void; + back(): void; + forward(): void; + refresh(): void; + dispose(): void; +} + +export type SandpackEnvironmentStatus = + | { + current: "LOADING"; + progress: string[]; + } + | { + current: "READY"; + } + | { + current: "ERROR"; + error: Error; + }; + +export interface SandpackEnvironment { + type: "bundler" | "static" | "vm"; + status: SandpackEnvironmentStatus; + onStatusChange( + listener: (status: SandpackEnvironmentStatus) => void + ): () => void; + createPreview(): SandpackPreview; + fs: SandpackFileSystem; + restart: () => void; + dispose(): void; +} diff --git a/sandpack-environments/src/utils.ts b/sandpack-environments/src/utils.ts new file mode 100644 index 000000000..bcef942a8 --- /dev/null +++ b/sandpack-environments/src/utils.ts @@ -0,0 +1,31 @@ +export function normalizePath(pathToNormalize: string): string { + // Normalize the path and ensure it starts with a slash + const withLeadingSlash = pathToNormalize.startsWith("/") + ? pathToNormalize + : "/" + pathToNormalize; + + // Ensure path does not end with a slash (unless it's the root path) + return withLeadingSlash === "/" + ? withLeadingSlash + : withLeadingSlash.endsWith("/") + ? withLeadingSlash.slice(0, -1) + : withLeadingSlash; +} + +export function debounce unknown>( + fn: T, + delay: number +): (...args: Parameters) => void { + // 'timer' will hold the timeout id for the pending function execution. + let timer: ReturnType; + + return (...args: Parameters): void => { + // If there is an existing timer, clear it to cancel the previous scheduled call. + clearTimeout(timer); + + // Set a new timeout to call the function after the specified delay. + timer = setTimeout(() => { + fn(...args); + }, delay); + }; +} diff --git a/sandpack-environments/tsconfig.json b/sandpack-environments/tsconfig.json new file mode 100644 index 000000000..5b03a94ec --- /dev/null +++ b/sandpack-environments/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "lib": ["es2015", "es2016", "es2017", "ES2019", "dom"], + "strict": true, + "module": "ESNext", + "target": "ES2015", + "moduleResolution": "bundler", + "sourceMap": false, + "emitDeclarationOnly": true, + "declaration": true, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "noImplicitAny": true, + "typeRoots": ["node_modules/@types"], + "skipLibCheck": true + }, + "include": ["./src"], + "exclude": ["node_modules", "src/**/*.test.ts"] +} diff --git a/sandpack-react/package.json b/sandpack-react/package.json index 74b513ce8..96e2548b4 100644 --- a/sandpack-react/package.json +++ b/sandpack-react/package.json @@ -58,7 +58,8 @@ "@codemirror/language": "^6.3.2", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.7.1", - "@codesandbox/sandpack-client": "^2.19.8", + "@codesandbox/sandpack-environments": "^0.1.0", + "@codesandbox/sdk": "workspace:*", "@lezer/highlight": "^1.1.3", "@react-hook/intersection-observer": "^3.1.1", "@stitches/core": "^1.2.6", @@ -91,7 +92,7 @@ "typescript": "^5.2.2" }, "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18", - "react-dom": "^16.8.0 || ^17 || ^18" + "react": "^18", + "react-dom": "^18" } } diff --git a/sandpack-react/src/Playground.stories.tsx b/sandpack-react/src/Playground.stories.tsx index 1cbb2b563..28f9e33f4 100644 --- a/sandpack-react/src/Playground.stories.tsx +++ b/sandpack-react/src/Playground.stories.tsx @@ -1,26 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import React from "react"; -import { - SandpackCodeEditor, - SandpackFileExplorer, - SandpackLayout, - SandpackProvider, -} from "./"; +import { Sandpack } from "./"; export default { title: "Intro/Playground", }; export const Basic: React.FC = () => { - return ( -
- - - - - - -
- ); + return ; }; diff --git a/sandpack-react/src/components/CodeEditor/CodeMirror.tsx b/sandpack-react/src/components/CodeEditor/CodeMirror.tsx index 8fae66401..33ca6c7c5 100644 --- a/sandpack-react/src/components/CodeEditor/CodeMirror.tsx +++ b/sandpack-react/src/components/CodeEditor/CodeMirror.tsx @@ -131,10 +131,6 @@ export const CodeMirror = React.forwardRef( ); const classNames = useClassNames(); - const { - listen, - sandpack: { autoReload }, - } = useSandpack(); const prevExtension = React.useRef([]); const prevExtensionKeymap = React.useRef([]); @@ -336,7 +332,6 @@ export const CodeMirror = React.forwardRef( themeId, readOnly, useStaticReadOnly, - autoReload, ]); React.useEffect( @@ -396,13 +391,15 @@ export const CodeMirror = React.forwardRef( // eslint-disable-next-line react-hooks/exhaustive-deps }, [code]); + /* + TODO: Allow sending generic action messages? React.useEffect( function messageToInlineError() { if (!showInlineErrors) return; const unsubscribe = listen((message) => { const view = cmView.current; - + if (message.type === "success") { view?.dispatch({ // @ts-ignore @@ -425,7 +422,7 @@ export const CodeMirror = React.forwardRef( }, [listen, showInlineErrors] ); - +*/ const handleContainerKeyDown = (evt: React.KeyboardEvent): void => { if (evt.key === "Enter" && cmView.current) { evt.preventDefault(); diff --git a/sandpack-react/src/components/CodeEditor/index.tsx b/sandpack-react/src/components/CodeEditor/index.tsx index a8b13b316..b08a3b8fc 100644 --- a/sandpack-react/src/components/CodeEditor/index.tsx +++ b/sandpack-react/src/components/CodeEditor/index.tsx @@ -1,7 +1,9 @@ import type { Extension } from "@codemirror/state"; import type { KeyBinding } from "@codemirror/view"; -import { forwardRef } from "react"; +import { forwardRef, useEffect, useRef, useState } from "react"; +import { useSandbox } from "../../contexts/SandpackSandboxContext"; +import { useSandpackState } from "../../contexts/SandpackStateContext"; import { useActiveCode } from "../../hooks/useActiveCode"; import { useSandpack } from "../../hooks/useSandpack"; import type { CustomLanguage, SandpackInitMode } from "../../types"; @@ -81,60 +83,103 @@ export const SandpackCodeEditor = forwardRef( }, ref ) => { - const { sandpack } = useSandpack(); - const { code, updateCode, readOnly: readOnlyFile } = useActiveCode(); - const { activeFile, status, editorState } = sandpack; - const shouldShowTabs = showTabs ?? sandpack.visibleFiles.length > 1; - + const { environment: env } = useSandbox(); + const useCodeRef = useRef(""); + const state = useSandpackState(); + const [code, setCode] = useState(""); + // const { code, updateCode, readOnly: readOnlyFile } = useActiveCode(); + const shouldShowTabs = false; // showTabs ?? sandpack.visibleFiles.length > 1; const classNames = useClassNames(); - const handleCodeUpdate = ( - newCode: string, - shouldUpdatePreview = true - ): void => { - updateCode(newCode, shouldUpdatePreview); + useEffect(() => { + if (state?.activeFile) { + env.fs.readFile(state.activeFile).then((content) => { + const code = + typeof content === "string" + ? content + : new TextDecoder("utf-8").decode(content); + + useCodeRef.current = code; + + setCode(code); + }); + } + }, [state?.activeFile]); + + useEffect(() => { + const saveListener = async (event: KeyboardEvent) => { + if ( + state.activeFile && + (event.metaKey || event.ctrlKey) && + event.key.toLowerCase() === "s" + ) { + event.preventDefault(); + + const content = useCodeRef.current; + const metadata = env.fs.readFileMetadata(state.activeFile); + await state.triggerChange({ + type: "update", + path: state.activeFile, + content, + metadata, + }); + env.fs.writeFile(state.activeFile, useCodeRef.current); + } + }; + + window.addEventListener("keydown", saveListener); + + return () => { + window.removeEventListener("keydown", saveListener); + }; + }, [state]); + + const handleCodeUpdate = async (newCode: string) => { + useCodeRef.current = newCode; + + if (env.type === "vm" || !state?.activeFile) { + return; + } + + env.fs.writeFile(state.activeFile, newCode); }; - const activeFileUniqueId = useSandpackId(); + if (!state.activeFile) { + return null; + } return ( - {shouldShowTabs && ( - - )} + {shouldShowTabs && }
- handleCodeUpdate(newCode, sandpack.autoReload ?? true) - } - readOnly={readOnly || readOnlyFile} + filePath={state.activeFile} + // TODO: Rather pass component specific options from the top always + initMode={"immediate" /*initMode || sandpack.initMode*/} + onCodeUpdate={(newCode: string) => handleCodeUpdate(newCode)} + readOnly={false /*readOnly || readOnlyFile*/} showInlineErrors={showInlineErrors} showLineNumbers={showLineNumbers} showReadOnly={showReadOnly} wrapContent={wrapContent} /> - {showRunButton && (!sandpack.autoReload || status === "idle") ? ( + {/*showRunButton && (!sandpack.autoReload || status === "idle") ? ( - ) : null} + ) : null*/}
); diff --git a/sandpack-react/src/components/FileExplorer/Directory.tsx b/sandpack-react/src/components/FileExplorer/Directory.tsx deleted file mode 100644 index 9f9730a6c..000000000 --- a/sandpack-react/src/components/FileExplorer/Directory.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import type { SandpackBundlerFiles } from "@codesandbox/sandpack-client"; -import * as React from "react"; - -import type { SandpackOptions } from "../../types"; - -import { File } from "./File"; -import { ModuleList } from "./ModuleList"; - -import type { SandpackFileExplorerProp } from "."; - -export interface Props extends SandpackFileExplorerProp { - prefixedPath: string; - files: SandpackBundlerFiles; - selectFile: (path: string) => void; - activeFile: NonNullable; - depth: number; - visibleFiles: NonNullable; -} - -export const Directory: React.FC = ({ - prefixedPath, - files, - selectFile, - activeFile, - depth, - autoHiddenFiles, - visibleFiles, - initialCollapsedFolder, -}) => { - const [open, setOpen] = React.useState( - !initialCollapsedFolder?.includes(prefixedPath) - ); - - const toggle = (): void => setOpen((prev) => !prev); - - return ( -
- - - {open && ( - - )} -
- ); -}; diff --git a/sandpack-react/src/components/FileExplorer/ModuleList.tsx b/sandpack-react/src/components/FileExplorer/ModuleList.tsx index 6b9e9f5e1..b1d647ab2 100644 --- a/sandpack-react/src/components/FileExplorer/ModuleList.tsx +++ b/sandpack-react/src/components/FileExplorer/ModuleList.tsx @@ -1,65 +1,144 @@ -import type { SandpackBundlerFiles } from "@codesandbox/sandpack-client"; +import type { DirectoryEntry } from "@codesandbox/sandpack-environments"; import * as React from "react"; +import { useSandbox } from "../../contexts/SandpackSandboxContext"; import type { SandpackOptions } from "../../types"; -import { Directory } from "./Directory"; import { File } from "./File"; -import { fromPropsToModules } from "./utils"; -import type { SandpackFileExplorerProp } from "."; - -export interface ModuleListProps extends SandpackFileExplorerProp { - prefixedPath: string; - files: SandpackBundlerFiles; +export interface ModuleListProps { + path: string; selectFile: (path: string) => void; activeFile: NonNullable; depth?: number; visibleFiles: NonNullable; + /** + * enable auto hidden file in file explorer + * + * @description set with hidden property in files property + * @default false + */ + autoHiddenFiles?: boolean; + + initialCollapsedFolder?: string[]; +} + +function join(...paths: string[]): string { + return paths.join("/").replace(/\/+/g, "/"); } export const ModuleList: React.FC = ({ depth = 0, activeFile, selectFile, - prefixedPath, - files, autoHiddenFiles, visibleFiles, initialCollapsedFolder, + path, }) => { - const { directories, modules } = fromPropsToModules({ - visibleFiles, - autoHiddenFiles, - prefixedPath, - files, - }); + const { environment: env } = useSandbox(); + const [entries, setEntries] = React.useState([]); + + React.useEffect(() => { + const updateEntries = () => { + env.fs.readDirectory(path).then((entries) => { + setEntries(entries); + }); + }; + + updateEntries(); + + return env.fs.watchDirectory(path, updateEntries); + }, []); + + const { directories, files } = React.useMemo( + () => + entries.reduce<{ + directories: string[]; + files: string[]; + }>( + (acc, entry) => { + const { name, type } = entry; + + if (type === "directory") { + acc.directories.push(name); + } else { + acc.files.push(name); + } + + return acc; + }, + { directories: [], files: [] } + ), + [entries] + ); return (
- {directories.map((dir) => ( + {directories.map((directoryPath) => ( ))} - {modules.map((file) => ( + {files.map((file) => ( ))}
); }; + +export type DirectoryProps = { + prefixedPath: string; + path: string; + selectFile: (path: string) => void; + activeFile: NonNullable; + depth: number; + visibleFiles: NonNullable; +} & Pick; + +export const Directory: React.FC = ({ + selectFile, + activeFile, + depth, + path, + autoHiddenFiles, + visibleFiles, + initialCollapsedFolder, +}) => { + const [open, setOpen] = React.useState(false); + + const toggle = (): void => setOpen((prev) => !prev); + + return ( +
+ + + {open && ( + + )} +
+ ); +}; diff --git a/sandpack-react/src/components/FileExplorer/index.tsx b/sandpack-react/src/components/FileExplorer/index.tsx index b3652e606..22044c822 100644 --- a/sandpack-react/src/components/FileExplorer/index.tsx +++ b/sandpack-react/src/components/FileExplorer/index.tsx @@ -1,11 +1,11 @@ -import type { SandpackBundlerFiles } from "@codesandbox/sandpack-client"; import * as React from "react"; -import { useSandpack } from "../../hooks/useSandpack"; +import { useSandpackState } from "../../contexts/SandpackStateContext"; import { css } from "../../styles"; import { useClassNames } from "../../utils/classNames"; import { stackClassName } from "../common"; +import type { ModuleListProps } from "./ModuleList"; import { ModuleList } from "./ModuleList"; const fileExplorerClassName = css({ @@ -14,17 +14,10 @@ const fileExplorerClassName = css({ height: "100%", }); -export interface SandpackFileExplorerProp { - /** - * enable auto hidden file in file explorer - * - * @description set with hidden property in files property - * @default false - */ - autoHiddenFiles?: boolean; - - initialCollapsedFolder?: string[]; -} +export type SandpackFileExplorerProp = Pick< + ModuleListProps, + "initialCollapsedFolder" | "autoHiddenFiles" +>; export const SandpackFileExplorer = ({ className, @@ -33,46 +26,9 @@ export const SandpackFileExplorer = ({ ...props }: SandpackFileExplorerProp & React.HTMLAttributes): JSX.Element | null => { - const { - sandpack: { - status, - updateFile, - deleteFile, - activeFile, - files, - openFile, - visibleFilesFromProps, - }, - listen, - } = useSandpack(); + const sandpackState = useSandpackState(); const classNames = useClassNames(); - React.useEffect( - function watchFSFilesChanges() { - if (status !== "running") return; - - const unsubscribe = listen((message) => { - if (message.type === "fs/change") { - updateFile(message.path, message.content, false); - } - - if (message.type === "fs/remove") { - deleteFile(message.path, false); - } - }); - - return unsubscribe; - }, - [status] - ); - - const orderedFiles = Object.keys(files) - .sort() - .reduce((obj, key) => { - obj[key] = files[key]; - return obj; - }, {}); - return (
{ + sandpackState.setActiveFile(filepath); + }} + visibleFiles={[]} />
diff --git a/sandpack-react/src/components/FileExplorer/util.test.ts b/sandpack-react/src/components/FileExplorer/util.test.ts deleted file mode 100644 index 3708639d2..000000000 --- a/sandpack-react/src/components/FileExplorer/util.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { ModuleListProps } from "./ModuleList"; -import { fromPropsToModules } from "./utils"; - -const defaultProps: ModuleListProps = { - files: { - "/src/component/index.js": { code: "", hidden: true }, - "/src/folder/index.js": { code: "", hidden: true }, - "/component/index.js": { code: "", hidden: true }, - "/component/src/index.js": { code: "", hidden: true }, - "/hidden-folder/index.js": { code: "", hidden: true }, - "/non-hidden-folder/index.js": { code: "", hidden: false }, - "/index.js": { code: "", hidden: true }, - "/App.js": { code: "", hidden: false }, - }, - autoHiddenFiles: false, - visibleFiles: [], - prefixedPath: "/", - activeFile: "", - selectFile: () => { - // - }, -}; - -describe(fromPropsToModules, () => { - it("returns a list of unique folder", () => { - expect(fromPropsToModules(defaultProps).directories).toEqual([ - "/src/", - "/component/", - "/hidden-folder/", - "/non-hidden-folder/", - ]); - }); - - it("returns only the root files", () => { - expect(fromPropsToModules(defaultProps).modules).toEqual([ - "/index.js", - "/App.js", - ]); - }); - - it("returns the folder from a subfolder", () => { - const input: ModuleListProps = { - ...defaultProps, - prefixedPath: "/src/", - }; - - expect(fromPropsToModules(input).directories).toEqual([ - "/src/component/", - "/src/folder/", - ]); - }); - - it("returns only the files from the visibleFiles prop (autoHiddenFiles)", () => { - const input: ModuleListProps = { - ...defaultProps, - autoHiddenFiles: true, - visibleFiles: ["/index.js", "/src/component/index.js"], - }; - - expect(fromPropsToModules(input)).toEqual({ - directories: ["/src/"], - modules: ["/index.js"], - }); - }); - - it("returns only the non-hidden files (autoHiddenFiles)", () => { - const input: ModuleListProps = { - ...defaultProps, - autoHiddenFiles: true, - visibleFiles: [], - }; - - expect(fromPropsToModules(input)).toEqual({ - directories: ["/non-hidden-folder/"], - modules: ["/App.js"], - }); - }); -}); diff --git a/sandpack-react/src/components/FileExplorer/utils.ts b/sandpack-react/src/components/FileExplorer/utils.ts deleted file mode 100644 index 45ec12215..000000000 --- a/sandpack-react/src/components/FileExplorer/utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { SandpackBundlerFiles } from "@codesandbox/sandpack-client"; - -export const fromPropsToModules = ({ - autoHiddenFiles, - visibleFiles, - files, - prefixedPath, -}: { - prefixedPath: string; - files: SandpackBundlerFiles; - autoHiddenFiles?: boolean; - visibleFiles: string[]; -}): { directories: string[]; modules: string[] } => { - const hasVisibleFilesOption = visibleFiles.length > 0; - - /** - * When visibleFiles or activeFile are set, the hidden and active flags on the files prop are ignored. - */ - const filterByHiddenProperty = autoHiddenFiles && !hasVisibleFilesOption; - const filterByVisibleFilesOption = autoHiddenFiles && !!hasVisibleFilesOption; - - const fileListWithoutPrefix = Object.keys(files) - .filter((filePath) => { - const isValidatedPath = filePath.startsWith(prefixedPath); - if (filterByVisibleFilesOption) { - return isValidatedPath && visibleFiles.includes(filePath); - } - - if (filterByHiddenProperty) { - return isValidatedPath && !files[filePath]?.hidden; - } - - return isValidatedPath; - }) - .map((file) => file.substring(prefixedPath.length)); - - const directories = new Set( - fileListWithoutPrefix - .filter((file) => file.includes("/")) - .map((file) => `${prefixedPath}${file.split("/")[0]}/`) - ); - - const modules = fileListWithoutPrefix - .filter((file) => !file.includes("/")) - .map((file) => `${prefixedPath}${file}`); - - return { directories: Array.from(directories), modules }; -}; diff --git a/sandpack-react/src/components/Navigator/index.tsx b/sandpack-react/src/components/Navigator/index.tsx index f97be2db2..1a9cf1dfa 100644 --- a/sandpack-react/src/components/Navigator/index.tsx +++ b/sandpack-react/src/components/Navigator/index.tsx @@ -1,6 +1,6 @@ +import type * as sandpackEnv from "@codesandbox/sandpack-environments"; import * as React from "react"; -import { useSandpack } from "../../hooks/useSandpack"; import { css } from "../../styles"; import { buttonClassName, iconClassName } from "../../styles/shared"; import { useClassNames } from "../../utils/classNames"; @@ -44,23 +44,23 @@ const inputClassName = css({ }); export interface NavigatorProps { - clientId: string; onURLChange?: (newURL: string) => void; startRoute?: string; + preview: sandpackEnv.SandpackPreview; } export const Navigator = ({ - clientId, onURLChange, className, startRoute, + preview, ...props }: NavigatorProps & React.HTMLAttributes): JSX.Element => { const [baseUrl, setBaseUrl] = React.useState(""); - const { sandpack, dispatch, listen } = useSandpack(); const [relativeUrl, setRelativeUrl] = React.useState( - startRoute ?? sandpack.startRoute ?? "/" + // TODO: This should rather come from the new useOptions hook + startRoute ?? "/" ); const [backEnabled, setBackEnabled] = React.useState(false); @@ -68,23 +68,22 @@ export const Navigator = ({ const classNames = useClassNames(); - React.useEffect(() => { - const unsub = listen((message) => { - if (message.type === "urlchange") { - const { url, back, forward } = message; - - const [newBaseUrl, newRelativeUrl] = splitUrl(url); - - setBaseUrl(newBaseUrl); - setRelativeUrl(newRelativeUrl); - setBackEnabled(back); - setForwardEnabled(forward); - } - }, clientId); - - return (): void => unsub(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + React.useEffect( + () => + preview.onMessage((message) => { + if (message.type === "urlchange") { + const { url, back, forward } = message; + + const [newBaseUrl, newRelativeUrl] = splitUrl(url); + + setBaseUrl(newBaseUrl); + setRelativeUrl(newRelativeUrl); + setBackEnabled(back); + setForwardEnabled(forward); + } + }), + [preview] + ); const handleInputChange = (e: React.ChangeEvent): void => { const path = e.target.value.startsWith("/") @@ -107,15 +106,15 @@ export const Navigator = ({ }; const handleRefresh = (): void => { - dispatch({ type: "refresh" }); + preview.refresh(); }; const handleBack = (): void => { - dispatch({ type: "urlback" }); + preview.back(); }; const handleForward = (): void => { - dispatch({ type: "urlforward" }); + preview.forward(); }; const buttonsClassNames = classNames("button", [ diff --git a/sandpack-react/src/components/Preview/index.tsx b/sandpack-react/src/components/Preview/index.tsx index 8996a0aba..c0d27b84f 100644 --- a/sandpack-react/src/components/Preview/index.tsx +++ b/sandpack-react/src/components/Preview/index.tsx @@ -1,20 +1,7 @@ -import type { - SandpackClient, - SandpackMessage, -} from "@codesandbox/sandpack-client"; import * as React from "react"; -import { - useSandpackClient, - useSandpackNavigation, - useSandpackShell, -} from "../../hooks"; +import { usePreview } from "../../hooks/usePreview"; import { css, THEME_PREFIX } from "../../styles"; -import { - buttonClassName, - iconStandaloneClassName, - roundedButtonClassName, -} from "../../styles/shared"; import { useClassNames } from "../../utils/classNames"; import { Navigator } from "../Navigator"; import { ErrorOverlay } from "../common/ErrorOverlay"; @@ -22,8 +9,7 @@ import { LoadingOverlay } from "../common/LoadingOverlay"; import { OpenInCodeSandboxButton } from "../common/OpenInCodeSandboxButton"; import { RoundedButton } from "../common/RoundedButton"; import { SandpackStack } from "../common/Stack"; -import { RefreshIcon, RestartIcon } from "../icons"; -import { SignOutIcon } from "../icons"; +import { RefreshIcon } from "../icons"; export interface PreviewProps { style?: React.CSSProperties; @@ -83,146 +69,120 @@ const previewActionsClassName = css({ gap: "$space$2", }); -export interface SandpackPreviewRef { - /** - * Retrieve the current Sandpack client instance from preview - */ - getClient: () => InstanceType | null; - /** - * Returns the client id, which will be used to - * initialize a client in the main Sandpack context - */ - clientId: string; -} - -export const SandpackPreview = React.forwardRef< - SandpackPreviewRef, - PreviewProps & React.HTMLAttributes ->( - ( - { - showNavigator = false, - showRefreshButton = true, - showOpenInCodeSandbox = true, - showSandpackErrorOverlay = true, - showOpenNewtab = true, - showRestartButton = true, - actionsChildren = <>, - children, - className, - startRoute = "/", - ...props - }, - ref - ) => { - const { sandpack, listen, iframe, getClient, clientId, dispatch } = - useSandpackClient({ startRoute }); - const [iframeComputedHeight, setComputedAutoHeight] = React.useState< - number | null - >(null); - const { status } = sandpack; - const { refresh } = useSandpackNavigation(clientId); - const { restart } = useSandpackShell(clientId); - - const classNames = useClassNames(); - - React.useEffect(() => { - const unsubscribe = listen((message: SandpackMessage) => { - if (message.type === "resize") { - setComputedAutoHeight(message.height); - } - }); - - return unsubscribe; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - React.useImperativeHandle( - ref, - () => ({ - clientId: clientId, - getClient, - }), - [getClient, clientId] - ); - - const handleNewURL = (newUrl: string): void => { - if (!iframe.current) { - return; - } - - iframe.current.src = newUrl; - // eslint-disable-next-line react-hooks/exhaustive-deps - }; - - return ( - - {showNavigator && ( - - )} - -
-