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/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/package.json b/package.json index d1eaf64bd..8a0ede31d 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "sandpack-client", "sandpack-react", "sandpack-themes", + "examples/nextjs-app-dir", "website/*" ], "nohoist": [ @@ -26,7 +27,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", diff --git a/sandpack-client/package.json b/sandpack-client/package.json index d7f2a3548..7f0fb9449 100644 --- a/sandpack-client/package.json +++ b/sandpack-client/package.json @@ -56,6 +56,7 @@ ], "dependencies": { "@codesandbox/nodebox": "0.1.8", + "@codesandbox/sdk": "0.0.0-alpha.8", "buffer": "^6.0.3", "dequal": "^2.0.2", "mime-db": "^1.52.0", 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..e639eed98 100644 --- a/sandpack-client/src/clients/index.ts +++ b/sandpack-client/src/clients/index.ts @@ -22,6 +22,10 @@ export async function loadSandpackClient( 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/vm/client.utils.test.ts b/sandpack-client/src/clients/vm/client.utils.test.ts new file mode 100644 index 000000000..40443a53b --- /dev/null +++ b/sandpack-client/src/clients/vm/client.utils.test.ts @@ -0,0 +1,61 @@ +import { findStartScriptPackageJson } from "./client.utils"; + +describe(findStartScriptPackageJson, () => { + it("should parse a regular command", () => { + expect( + findStartScriptPackageJson(JSON.stringify({ scripts: { start: "node" } })) + ).toEqual(["node", [], { env: {} }]); + }); + + it("should parse a regular command with arguments", () => { + expect( + findStartScriptPackageJson( + JSON.stringify({ scripts: { start: "node dev --foo" } }) + ) + ).toEqual(["node", ["dev", "--foo"], { env: {} }]); + }); + + it("should get dev script first", () => { + expect( + findStartScriptPackageJson( + JSON.stringify({ + scripts: { start: "node start --foo", dev: "node dev --foo" }, + }) + ) + ).toEqual(["node", ["dev", "--foo"], { env: {} }]); + }); + + it("should parse env vars", () => { + expect( + findStartScriptPackageJson( + JSON.stringify({ + scripts: { start: "NODE=1 ANOTHER=2 node start --foo" }, + }) + ) + ).toEqual([ + "node", + ["start", "--foo"], + { env: { NODE: "1", ANOTHER: "2" } }, + ]); + }); + + it("should parse a single env var", () => { + expect( + findStartScriptPackageJson( + JSON.stringify({ + scripts: { start: "NODE=1 node start --foo" }, + }) + ) + ).toEqual(["node", ["start", "--foo"], { env: { NODE: "1" } }]); + }); + + it("should parse a single env var and a single commmand", () => { + expect( + findStartScriptPackageJson( + JSON.stringify({ + scripts: { start: "NODE=1 node" }, + }) + ) + ).toEqual(["node", [], { env: { NODE: "1" } }]); + }); +}); 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..5f223f61f --- /dev/null +++ b/sandpack-client/src/clients/vm/client.utils.ts @@ -0,0 +1,139 @@ +import type { SandboxWithoutClient } from "@codesandbox/sdk/dist/esm/sandbox"; + +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: SandboxWithoutClient["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 TYPES = { + FILE: 0, + FOLDER: 1, + }; + + 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 === TYPES.FILE) { + results.push({ + path: fullPath, + content: await fs.readFile(fullPath), + }); + } + + // Recursively scan subdirectories + if (entry.type === TYPES.FOLDER) { + 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/vm/iframe.utils.ts b/sandpack-client/src/clients/vm/iframe.utils.ts new file mode 100644 index 000000000..30724ce99 --- /dev/null +++ b/sandpack-client/src/clients/vm/iframe.utils.ts @@ -0,0 +1,65 @@ +import type { ClientOptions } from "../.."; +import { createError } from "../.."; +import { nullthrows } from "../.."; + +export async function loadPreviewIframe( + iframe: HTMLIFrameElement, + url: string +): Promise { + const { contentWindow } = iframe; + + nullthrows( + contentWindow, + "Failed to await preview iframe: no content window found" + ); + + const TIME_OUT = 90_000; + const MAX_MANY_TIRES = 20; + let tries = 0; + let timeout: ReturnType; + + return new Promise((resolve, reject) => { + const triesToSetUrl = (): void => { + const onLoadPage = (): void => { + clearTimeout(timeout); + tries = MAX_MANY_TIRES; + resolve(); + + iframe.removeEventListener("load", onLoadPage); + }; + + if (tries >= MAX_MANY_TIRES) { + reject(createError(`Could not able to connect to preview.`)); + + return; + } + + iframe.setAttribute("src", url); + + timeout = setTimeout(() => { + triesToSetUrl(); + iframe.removeEventListener("load", onLoadPage); + }, TIME_OUT); + + tries = tries + 1; + + iframe.addEventListener("load", onLoadPage); + }; + + iframe.addEventListener("error", () => reject(new Error("Iframe error"))); + iframe.addEventListener("abort", () => reject(new Error("Aborted"))); + + triesToSetUrl(); + }); +} + +export const setPreviewIframeProperties = ( + iframe: HTMLIFrameElement, + options: ClientOptions +): void => { + iframe.style.border = "0"; + iframe.style.width = options.width || "100%"; + iframe.style.height = options.height || "100%"; + iframe.style.overflow = "hidden"; + iframe.allow = "cross-origin-isolated"; +}; diff --git a/sandpack-client/src/clients/vm/index.ts b/sandpack-client/src/clients/vm/index.ts new file mode 100644 index 000000000..5ac9d37a6 --- /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 { FilesMap } from "@codesandbox/nodebox"; +import { connectToSandbox } from "@codesandbox/sdk/browser"; +import type { PortInfo } from "@codesandbox/sdk/dist/esm/ports"; +import type { SandboxWithoutClient } from "@codesandbox/sdk/dist/esm/sandbox"; + +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 class SandpackVM extends SandpackClient { + private emitter: EventEmitter; + private sandbox!: SandboxWithoutClient; + public iframe!: HTMLIFrameElement; + + 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/vm/inject-scripts/historyListener.ts b/sandpack-client/src/clients/vm/inject-scripts/historyListener.ts new file mode 100644 index 000000000..142bd814a --- /dev/null +++ b/sandpack-client/src/clients/vm/inject-scripts/historyListener.ts @@ -0,0 +1,85 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/explicit-function-return-type, no-restricted-globals, @typescript-eslint/no-explicit-any */ + +export function setupHistoryListeners({ + scope, +}: { + scope: { channelId: string }; +}) { + // @ts-ignore + const origHistoryProto = window.history.__proto__; + + const historyList: Array<{ state: string; url: string }> = []; + let historyPosition = 0; + + const dispatchMessage = (url: string) => { + parent.postMessage( + { + type: "urlchange", + url, + back: historyPosition > 0, + forward: historyPosition < historyList.length - 1, + channelId: scope.channelId, + }, + "*" + ); + }; + + function pushHistory(url: string, state: string) { + // remove "future" locations + historyList.splice(historyPosition + 1); + historyList.push({ url, state }); + historyPosition = historyList.length - 1; + } + + Object.assign(window.history, { + go(delta: number) { + const newPos = historyPosition + delta; + if (newPos >= 0 && newPos <= historyList.length - 1) { + historyPosition = newPos; + + const { url, state } = historyList[historyPosition]; + origHistoryProto.replaceState.call(window.history, state, "", url); + + const newURL = document.location.href; + dispatchMessage(newURL); + + window.dispatchEvent(new PopStateEvent("popstate", { state })); + } + }, + + back() { + window.history.go(-1); + }, + + forward() { + window.history.go(1); + }, + + pushState(state: string, title: string, url: string) { + origHistoryProto.replaceState.call(window.history, state, title, url); + pushHistory(url, state); + dispatchMessage(document.location.href); + }, + + replaceState(state: string, title: string, url: string) { + origHistoryProto.replaceState.call(window.history, state, title, url); + historyList[historyPosition] = { state, url }; + dispatchMessage(document.location.href); + }, + }); + + interface NavigationMessage { + type: "urlback" | "urlforward" | "refresh"; + } + function handleMessage({ data }: { data: NavigationMessage }) { + if (data.type === "urlback") { + history.back(); + } else if (data.type === "urlforward") { + history.forward(); + } else if (data.type === "refresh") { + document.location.reload(); + } + } + + window.addEventListener("message", handleMessage); +} diff --git a/sandpack-client/src/clients/vm/inject-scripts/index.ts b/sandpack-client/src/clients/vm/inject-scripts/index.ts new file mode 100644 index 000000000..86bb62cc5 --- /dev/null +++ b/sandpack-client/src/clients/vm/inject-scripts/index.ts @@ -0,0 +1,36 @@ +/* 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"; + +import { setupHistoryListeners } from "./historyListener"; +import { watchResize } from "./resize.js"; + +const scripts = [ + { code: setupHistoryListeners.toString(), id: "historyListener" }, + { + code: "function consoleHook({ scope }) {" + consoleHook + "\n};", + id: "consoleHook", + }, + { code: watchResize.toString(), id: "watchResize" }, +]; + +export const injectScriptToIframe = ( + iframe: HTMLIFrameElement, + channelId: string +): void => { + scripts.forEach(({ code, id }) => { + const message: InjectMessage = { + uid: id, + type: INJECT_MESSAGE_TYPE, + code: `exports.activate = ${code}`, + scope: { channelId }, + }; + + iframe.contentWindow?.postMessage(message, "*"); + }); +}; diff --git a/sandpack-client/src/clients/vm/inject-scripts/resize.ts b/sandpack-client/src/clients/vm/inject-scripts/resize.ts new file mode 100644 index 000000000..c3d41f4ed --- /dev/null +++ b/sandpack-client/src/clients/vm/inject-scripts/resize.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export function watchResize({ scope }: { scope: { channelId: string } }) { + let lastHeight = 0; + + function getDocumentHeight(): number { + if (typeof window === "undefined") return 0; + + const { body } = document; + const html = document.documentElement; + + return Math.max(body.scrollHeight, body.offsetHeight, html.offsetHeight); + } + + function sendResizeEvent() { + const height = getDocumentHeight(); + + if (lastHeight !== height) { + window.parent.postMessage( + { + type: "resize", + height, + codesandbox: true, + channelId: scope.channelId, + }, + "*" + ); + } + + lastHeight = height; + } + + sendResizeEvent(); + + let throttle: any; + const observer = new MutationObserver(() => { + if (throttle === undefined) { + sendResizeEvent(); + + throttle = setTimeout(() => { + throttle = undefined; + }, 300); + } + }); + + observer.observe(document, { + attributes: true, + childList: true, + subtree: true, + }); + + /** + * Ideally we should only use a `MutationObserver` to trigger a resize event, + * however, we noted that it's not 100% reliable, so we went for polling strategy as well + */ + setInterval(sendResizeEvent, 300); +} diff --git a/sandpack-client/src/clients/vm/types.ts b/sandpack-client/src/clients/vm/types.ts new file mode 100644 index 000000000..d5ab3322d --- /dev/null +++ b/sandpack-client/src/clients/vm/types.ts @@ -0,0 +1,77 @@ +import type { FilesMap } from "@codesandbox/nodebox"; + +import type { + BaseSandpackMessage, + SandpackErrorMessage, + SandpackLogLevel, +} from "../.."; + +type SandpackStandartMessages = + | { + type: "start"; + firstLoad?: boolean; + } + | { + type: "done"; + compilatonError: boolean; + }; + +type SandpackBundlerMessages = + | { + type: "compile"; + modules: FilesMap; + template?: string; + logLevel?: SandpackLogLevel; + } + | ({ + type: "action"; + action: "show-error"; + } & SandpackErrorMessage) + | { + type: "action"; + action: "notification"; + notificationType: "error"; + title: string; + }; + +type SandpackFSMessages = + | { type: "fs/change"; path: string; content: string } + | { type: "fs/remove"; path: string }; + +type SandpackURLsMessages = + | { + type: "urlchange"; + url: string; + back: boolean; + forward: boolean; + } + | { + type: "refresh"; + } + | { + type: "urlback"; + } + | { + type: "urlforward"; + }; + +export type SandpackVMMessage = BaseSandpackMessage & + ( + | SandpackStandartMessages + | SandpackURLsMessages + | SandpackBundlerMessages + | { type: "connected" } + | { + type: "stdout"; + payload: SandpackShellStdoutData; + } + | { type: "vm/progress"; data: string } + | { type: "vm/request_editor_url" } + | { type: "vm/response_editor_url"; data: string } + | SandpackFSMessages + ); + +export interface SandpackShellStdoutData { + data?: string; + type?: "out" | "err"; +} diff --git a/sandpack-client/src/types.ts b/sandpack-client/src/types.ts index 9e4fc8ce8..b7b1ba60d 100644 --- a/sandpack-client/src/types.ts +++ b/sandpack-client/src/types.ts @@ -1,5 +1,6 @@ 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 +77,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 +89,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 +183,10 @@ export interface BundlerState { transpiledModules: Record; } -export type SandpackMessage = SandpackRuntimeMessage | SandpackNodeMessage; +export type SandpackMessage = + | SandpackRuntimeMessage + | SandpackNodeMessage + | SandpackVMMessage; export type ListenerFunction = (msg: SandpackMessage) => void; export type UnsubscribeFunction = () => void; @@ -397,4 +407,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-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/FileExplorer/index.tsx b/sandpack-react/src/components/FileExplorer/index.tsx index b3652e606..184f08dfe 100644 --- a/sandpack-react/src/components/FileExplorer/index.tsx +++ b/sandpack-react/src/components/FileExplorer/index.tsx @@ -34,38 +34,10 @@ export const SandpackFileExplorer = ({ }: SandpackFileExplorerProp & React.HTMLAttributes): JSX.Element | null => { const { - sandpack: { - status, - updateFile, - deleteFile, - activeFile, - files, - openFile, - visibleFilesFromProps, - }, - listen, + sandpack: { activeFile, files, openFile, visibleFilesFromProps }, } = useSandpack(); 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) => { diff --git a/sandpack-react/src/components/common/LoadingOverlay.tsx b/sandpack-react/src/components/common/LoadingOverlay.tsx index bddefbd2b..89f57c175 100644 --- a/sandpack-react/src/components/common/LoadingOverlay.tsx +++ b/sandpack-react/src/components/common/LoadingOverlay.tsx @@ -56,10 +56,12 @@ export const LoadingOverlay: React.FC< } = useSandpack(); const [shouldShowStdout, setShouldShowStdout] = React.useState(false); - const loadingOverlayState = useLoadingOverlayState(clientId, loading); const progressMessage = useSandpackPreviewProgress({ clientId }); + const loadingOverlayState = useLoadingOverlayState(clientId, loading); const { logs: stdoutData } = useSandpackShellStdout({ clientId }); + const forking = progressMessage?.toLowerCase().includes("forking"); + React.useEffect(() => { let timer: NodeJS.Timer; if (progressMessage?.includes("Running")) { @@ -75,7 +77,7 @@ export const LoadingOverlay: React.FC< }; }, [progressMessage]); - if (loadingOverlayState === "HIDDEN") { + if (loadingOverlayState === "HIDDEN" && !forking) { return null; } @@ -161,7 +163,7 @@ export const LoadingOverlay: React.FC< ])} style={{ ...style, - opacity: stillLoading ? 1 : 0, + opacity: stillLoading ? 1 : forking ? 0.7 : 0, transition: `opacity ${FADE_ANIMATION_DURATION}ms ease-out`, }} {...props} diff --git a/sandpack-react/src/components/common/OpenInCodeSandboxButton/UnstyledOpenInCodeSandboxButton.tsx b/sandpack-react/src/components/common/OpenInCodeSandboxButton/UnstyledOpenInCodeSandboxButton.tsx index 966c6a79c..35dc05811 100644 --- a/sandpack-react/src/components/common/OpenInCodeSandboxButton/UnstyledOpenInCodeSandboxButton.tsx +++ b/sandpack-react/src/components/common/OpenInCodeSandboxButton/UnstyledOpenInCodeSandboxButton.tsx @@ -48,6 +48,10 @@ export const UnstyledOpenInCodeSandboxButton: React.FC< > = (props) => { const { sandpack } = useSandpack(); + if (sandpack.environment === "vm") { + return ; + } + if (sandpack.exportOptions) { return ; } @@ -55,6 +59,38 @@ export const UnstyledOpenInCodeSandboxButton: React.FC< return ; }; +export const ExportVMButton: React.FC> = ({ + children, + ...props +}) => { + const sandpack = useSandpack(); + + React.useEffect(() => { + return sandpack.listen((message) => { + if (message.type === "vm/response_editor_url") { + window.open(message.data, "_blank"); + } + }); + }); + + const submit = () => { + sandpack.dispatch({ + type: "vm/request_editor_url", + }); + }; + + return ( + + ); +}; + export const ExportToWorkspaceButton: React.FC< React.HtmlHTMLAttributes & { state: SandpackState } > = ({ children, state, ...props }) => { diff --git a/sandpack-react/src/contexts/sandpackContext.tsx b/sandpack-react/src/contexts/sandpackContext.tsx index 6cc910c16..8a3128ae5 100644 --- a/sandpack-react/src/contexts/sandpackContext.tsx +++ b/sandpack-react/src/contexts/sandpackContext.tsx @@ -21,6 +21,25 @@ export const SandpackProvider: React.FC = (props) => { clientOperations.initializeSandpackIframe(); }, []); + React.useEffect( + function watchFSFilesChanges() { + if (clientState.status !== "running") return; + + const unsubscribe = addListener((message) => { + if (message.type === "fs/change") { + fileOperations.updateFile(message.path, message.content, false); + } + + if (message.type === "fs/remove") { + fileOperations.deleteFile(message.path, false); + } + }); + + return unsubscribe; + }, + [clientState.status] + ); + return ( | string>; activeFile: TemplateFiles | string; shouldUpdatePreview: boolean; + templateID?: string; } interface FilesOperations { @@ -65,6 +67,20 @@ export const useFiles: UseFiles = (props) => { } }, [props.files, props.customSetup, props.template]); + useEffect( + function findActiveFileIfMissed() { + if (!state.activeFile) { + for (const file of DEFAULT_FILES_TO_OPEN) { + if (state.files[file]) { + setState((prev) => ({ ...prev, activeFile: file })); + break; + } + } + } + }, + [state] + ); + const updateFile = ( pathOrFiles: string | SandpackFiles, code?: string, diff --git a/sandpack-react/src/hooks/useSandpackPreviewProgress.ts b/sandpack-react/src/hooks/useSandpackPreviewProgress.ts index 972809a06..b323e2b3d 100644 --- a/sandpack-react/src/hooks/useSandpackPreviewProgress.ts +++ b/sandpack-react/src/hooks/useSandpackPreviewProgress.ts @@ -81,6 +81,8 @@ export const useSandpackPreviewProgress = ( mapProgressMessage(message.data, totalDependencies) ); } + } else if (message.type === "vm/progress") { + setLoadingMessage(message.data); } if (message.type === "done" && message.compilatonError === false) { diff --git a/sandpack-react/src/presets/Sandpack.tsx b/sandpack-react/src/presets/Sandpack.tsx index fa74f1b03..b48dcc825 100644 --- a/sandpack-react/src/presets/Sandpack.tsx +++ b/sandpack-react/src/presets/Sandpack.tsx @@ -86,15 +86,19 @@ export const Sandpack: SandpackInternal = ({ const [counter, setCounter] = React.useState(0); const hasRightColumn = options.showConsole || options.showConsoleButton; - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - const templateFiles = SANDBOX_TEMPLATES[template!] ?? {}; - const mode = ( - options?.layout - ? options?.layout - : "mode" in templateFiles - ? templateFiles.mode - : "preview" - ) as typeof options.layout; + function getMode() { + if (options?.layout) { + return options.layout; + } + + const templateFiles = SANDBOX_TEMPLATES[template!]; + if (typeof templateFiles === "object" && "mode" in templateFiles) { + return templateFiles.mode; + } + + return "preview"; + } + const mode = getMode(); const actionsChildren = options.showConsoleButton ? ( string; } /** @@ -273,7 +275,8 @@ export type SandboxEnvironment = | "vue-cli" | "static" | "solid" - | "node"; + | "node" + | "vm"; /** * @category Setup @@ -406,7 +409,9 @@ export type SandpackThemeProp = */ export type TemplateFiles = - keyof typeof SANDBOX_TEMPLATES[Name]["files"]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + keyof (typeof SANDBOX_TEMPLATES)[Name]["files"]; export interface SandpackInternal { < @@ -482,6 +487,7 @@ export interface SandpackInternalOptions< classes?: Record; experimental_enableServiceWorker?: boolean; experimental_enableStableServiceWorkerId?: boolean; + vmEnvironmentApiUrl?: (id: string) => string; } interface SandpackInternalProps< @@ -521,6 +527,7 @@ interface SandpackInternalProps< showReadOnly?: boolean; layout?: "preview" | "tests" | "console"; + vmEnvironmentApiUrl?: (id: string) => string; }; } @@ -643,8 +650,9 @@ export interface SandboxTemplate { dependencies: Record; devDependencies?: Record; entry?: string; - main: string; + main?: string; environment: SandboxEnvironment; + templateID?: string; } export type SandpackFiles = Record; diff --git a/sandpack-react/src/utils/sandpackUtils.ts b/sandpack-react/src/utils/sandpackUtils.ts index fe4516565..696bd1870 100644 --- a/sandpack-react/src/utils/sandpackUtils.ts +++ b/sandpack-react/src/utils/sandpackUtils.ts @@ -23,6 +23,7 @@ export interface SandpackContextInfo { files: Record; environment: SandboxEnvironment; shouldUpdatePreview: true; + templateID?: string; } /** @@ -72,7 +73,7 @@ export const getSandpackStateFromProps = ( }); } - if (visibleFiles.length === 0) { + if (visibleFiles.length === 0 && projectSetup.main) { // If no files are received, use the project setup / template visibleFiles = [projectSetup.main]; } @@ -99,22 +100,26 @@ export const getSandpackStateFromProps = ( visibleFiles.push(activeFile); } - const files = addPackageJSONIfNeeded( - projectSetup.files, - projectSetup.dependencies ?? {}, - projectSetup.devDependencies ?? {}, - projectSetup.entry - ); + let files = projectSetup.files; + + if (projectSetup.environment !== "vm") { + files = addPackageJSONIfNeeded( + projectSetup.files, + projectSetup.dependencies ?? {}, + projectSetup.devDependencies ?? {}, + projectSetup.entry + ); + } const existOpenPath = visibleFiles.filter((path) => files[path]); return { visibleFiles: existOpenPath, - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - activeFile: activeFile!, + activeFile: activeFile, files, environment: projectSetup.environment, shouldUpdatePreview: true, + templateID: projectSetup.templateID, }; }; @@ -210,6 +215,20 @@ const combineTemplateFilesToSetup = ({ ); } + /** + * Sandbox template id for VM env + */ + if (baseTemplate.templateID) { + return { + files: convertedFilesToBundlerFiles(files), + dependencies: customSetup?.dependencies ?? {}, + devDependencies: customSetup?.devDependencies, + entry: normalizePath(customSetup?.entry), + environment: customSetup?.environment || baseTemplate.environment, + templateID: baseTemplate.templateID, + }; + } + // If no setup and not files, the template is used entirely if (!customSetup && !files) { return baseTemplate; @@ -262,3 +281,26 @@ export const convertedFilesToBundlerFiles = ( return acc; }, {}); }; + +export const DEFAULT_FILES_TO_OPEN = [ + "/src/App.js", + "/src/App.tsx", + "/src/index.js", + "/src/index.ts", + "/src/index.tsx", + "/app/page.tsx", + "/app/page.jsx", + "/index.js", + "/index.ts", + "/src/main.tsx", + "/src/main.jsx", + "/main.ts", + "/src/main.rs", + "/src/lib.rs", + "/index.html", + "/src/index.html", + "/src/index.vue", + "/src/App.vue", + "/src/main.astro", + "/package.json", +]; diff --git a/website/codesandbox-theme-docs/src/index.tsx b/website/codesandbox-theme-docs/src/index.tsx index a85633965..4f70909b2 100644 --- a/website/codesandbox-theme-docs/src/index.tsx +++ b/website/codesandbox-theme-docs/src/index.tsx @@ -90,7 +90,10 @@ const Body = ({ "nextra-body-typesetting-article" )} > -
+
{breadcrumb} {body}
diff --git a/website/docs/next-env.d.ts b/website/docs/next-env.d.ts index 4f11a03dc..a4a7b3f5c 100644 --- a/website/docs/next-env.d.ts +++ b/website/docs/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/pages/building-your-application/configuring/typescript for more information. diff --git a/yarn.lock b/yarn.lock index 40687fa2c..1069afc0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2072,6 +2072,13 @@ outvariant "^1.4.0" strict-event-emitter "^0.4.3" +"@codesandbox/sdk@0.0.0-alpha.8": + version "0.0.0-alpha.8" + resolved "https://registry.yarnpkg.com/@codesandbox/sdk/-/sdk-0.0.0-alpha.8.tgz#5505bf2506dde23050535be39878a7cb11e262a5" + integrity sha512-HaR8Mlhy0ccfCBSEm+ly/qxkSI51yBqXqDJjrPLR3sKkquphSntnG4FMA1zw6e43ktslLioZ01w/K2+T7zBlUw== + dependencies: + "@hey-api/client-fetch" "^0.4.2" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -2353,6 +2360,11 @@ dependencies: client-only "^0.0.1" +"@hey-api/client-fetch@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@hey-api/client-fetch/-/client-fetch-0.4.2.tgz#02924579205dcfd4cb7c33db236007cb617ab30d" + integrity sha512-9BqcLTjsM3rWbads3afJkELS86vK7EqJvYgT429EVS9IO/kN75HEka3Ay/k142xCHSfXOuOShMdDam3nbG8wVA== + "@humanwhocodes/config-array@^0.11.8": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -4366,6 +4378,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== +"@types/node@18.15.11": + version "18.15.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" + integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== + "@types/node@^18.0.0": version "18.18.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.7.tgz#bb3a7068dc4ba421b6968f2a259298b3a4e129e8" @@ -4418,6 +4435,13 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.6.tgz#7cb33992049fd7340d5b10c0098e104184dfcd2a" integrity sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA== +"@types/react-dom@18.0.11": + version "18.0.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33" + integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw== + dependencies: + "@types/react" "*" + "@types/react-dom@^18.0.6": version "18.0.8" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.8.tgz#d2606d855186cd42cc1b11e63a71c39525441685" @@ -4434,6 +4458,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@18.0.37": + version "18.0.37" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.37.tgz#7a784e2a8b8f83abb04dc6b9ed9c9b4c0aee9be7" + integrity sha512-4yaZZtkRN3ZIQD3KSEwkfcik8s0SWV+82dlJot1AbGYHCzJkWP3ENBY6wYeDRmKZ6HkrgoGAmR2HqdwYGp6OEw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/resolve@1.20.2": version "1.20.2" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" @@ -16638,6 +16671,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + "typescript@>=3 < 6", typescript@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"