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"