Our Feature Flags course is now live!
Stackpack

WebContainer API

WebContainer API is what does most of the heavy lifting over here. It runs a server within the browser.

WebContainers are a browser-based runtime for executing Node.js applications and operating system commands, entirely inside your browser tab. - Introduction to WebContainers

Installation

Let us install the dependency first. A single dependency is what we need.

pnpm add @webcontainer/api

Usage

First of all, due to the technology of WebContainer, we have to set up a couple of headers in our vite.config.ts file.

Setting up Headers

vite.config.ts
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';
 
// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
  server: {
    headers: {
      'Cross-Origin-Embedder-Policy': 'require-corp',
      'Cross-Origin-Opener-Policy': 'same-origin',
    },
  },
});

Booting the WebContainer

Let us boot the WebContainer instance in the App component and store it in the state.

src/App.tsx
import { WebContainer } from '@webcontainer/api';
import React from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import CodeEditor from './components/code-editor';
 
export default function App() {
  const [webContainer, setWebContainer] = React.useState<WebContainer | null>(
    null,
  );
 
  React.useEffect(() => {
    const createWebContainer = async () => {
      const webContainerInstance = await WebContainer.boot();
      setWebContainer(webContainerInstance);
    };
 
    createWebContainer();
 
    // Ideally, we should clean up the WebContainer instance when the component is unmounted.
    // But there is an issue with the current implementation of WebContainer that prevents it from being torn down.
    // https://github.com/stackblitz/webcontainer-core/issues/1125
    // return () => {
    //   webContainer?.teardown();
    //   setWebContainer(null);
    // };
  }, []);
 
  return (
    <div className="h-dvh p-2">
      <PanelGroup direction="horizontal">
        <Panel>
          <PanelGroup direction="vertical">
            <Panel>
              <CodeEditor />
            </Panel>
            <PanelResizeHandle className="h-2 bg-blue-300" />
            <Panel>
              <div className="h-full border bg-red-100">Terminal</div>
            </Panel>
          </PanelGroup>
        </Panel>
        <PanelResizeHandle className="w-2 bg-blue-300" />
        <Panel>
          <div className="h-full border bg-red-100">Preview</div>
        </Panel>
      </PanelGroup>
    </div>
  );
}

The WebContainer API has its own file system format. So, we will also use the same file system and update our TEMPLATE to use the same. Do not worry much, and just copy-paste the below code.

We get most of the type safety from TypeScript, and we can use the same to define our template.

src/templates/react-vite.ts
import type { FileNode } from '@webcontainer/api';
 
export type Template = {
  files: Record<string, FileNode>;
  entry: string;
  visibleFiles: string[];
};
 
export const VITE_REACT_TEMPLATE: Template = {
  files: {
    'App.jsx': {
      file: {
        contents: `export default function App() {
    const data = "world"
  
    return <h1>Hello {data}</h1>
  }
  `,
      },
    },
 
    'index.jsx': {
      file: {
        contents: `import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
 
import App from "./App";
 
const root = createRoot(document.getElementById("root"));
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);`,
      },
    },
 
    'index.html': {
      file: {
        contents: `<!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Vite App</title>
    </head>
    <body>
      <div id="root"></div>
      <script type="module" src="/index.jsx"></script>
    </body>
  </html>
  `,
      },
    },
 
    'package.json': {
      file: {
        contents: `{
      "scripts": {
          "dev": "vite",
          "build": "vite build",
          "preview": "vite preview"
      },
      "dependencies": {
          "react": "^18.2.0",
          "react-dom": "^18.2.0"
      },
      "devDependencies": {
          "@vitejs/plugin-react": "3.1.0",
          "vite": "4.1.4",
          "esbuild-wasm": "0.17.12"
      }
  }`,
      },
    },
    'vite.config.js': {
      file: {
        contents: `import { defineConfig } from "vite";
  import react from "@vitejs/plugin-react";
  
  // https://vitejs.dev/config/
  export default defineConfig({
    plugins: [react()],
  });
  `,
      },
    },
  },
  entry: 'App.jsx',
  visibleFiles: ['App.jsx', 'index.jsx', 'index.html'],
};

WebContainer API also supports directories. But for our use case, we are only working with files. It does add a lot of complexity to the code, and we are trying to keep it simple for this course. Also, file systems contents support Uint8Array as well. You can consider it as a string. But for this course, again, we are only working with strings for the file contents.

We will also update the CodeEditor component to account for these changes.

src/components/code-editor.tsx
import { Editor } from '@monaco-editor/react';
import React from 'react';
import { VITE_REACT_TEMPLATE } from '../templates/react-vite';
import { getLanguageFromFileName } from '../utils/get-language-from-file-name';
import FileTabs from './file-tabs';
 
export default function CodeEditor() {
  const [activeFile, setActiveFile] = React.useState(
    () => VITE_REACT_TEMPLATE.entry,
  );
 
  const currentFile = VITE_REACT_TEMPLATE.files[activeFile];
  const language = getLanguageFromFileName(activeFile);
 
  return (
    <div className="h-full">
      <FileTabs
        files={VITE_REACT_TEMPLATE.visibleFiles}
        activeFile={activeFile}
        onFileChange={setActiveFile}
      />
      <Editor
        theme="vs-dark"
        path={activeFile}
        defaultValue={currentFile.file.contents as string} // Ideally, worry about the encoding in production, for our example, this is fine.
        defaultLanguage={language}
      />
    </div>
  );
}

Alright, you would see an error in the console that we caught about the multiple instances of WebContainer. This is a known issue with the WebContainer API. We have to wait for the issue to be resolved. But for now, we can ignore it.

Mount the filesystem to the WebContainer

Our webContainer instance currently has no filesystem mounted to it. Let us mount the VITE_REACT_TEMPLATE to the webContainer instance.

src/App.tsx
import { WebContainer } from '@webcontainer/api';
import React from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import CodeEditor from './components/code-editor';
import { VITE_REACT_TEMPLATE } from './templates/react-vite';
 
export default function App() {
  const [webContainer, setWebContainer] = React.useState<WebContainer | null>(
    null,
  );
 
  React.useEffect(() => {
    const createWebContainer = async () => {
      const webContainerInstance = await WebContainer.boot();
      await webContainerInstance.mount(VITE_REACT_TEMPLATE.files);
      setWebContainer(webContainerInstance);
    };
 
    createWebContainer();
 
    // Ideally, we should clean up the WebContainer instance when the component is unmounted.
    // But there is an issue with the current implementation of WebContainer that prevents it from being torn down.
    // https://github.com/stackblitz/webcontainer-core/issues/1125
    // return () => {
    //   webContainer?.teardown();
    //   setWebContainer(null);
    // };
  }, []);
 
  return (
    <div className="h-dvh p-2">
      <PanelGroup direction="horizontal">
        <Panel>
          <PanelGroup direction="vertical">
            <Panel>
              <CodeEditor />
            </Panel>
            <PanelResizeHandle className="h-2 bg-blue-300" />
            <Panel>
              <div className="h-full border bg-red-100">Terminal</div>
            </Panel>
          </PanelGroup>
        </Panel>
        <PanelResizeHandle className="w-2 bg-blue-300" />
        <Panel>
          <div className="h-full border bg-red-100">Preview</div>
        </Panel>
      </PanelGroup>
    </div>
  );
}

Great, we have now mounted the files of VITE_REACT_TEMPLATE to the webContainer instance.

In the next two sections, we will see wiring up the terminal and the preview section with the WebContainer API.

At this point, our code should match the code in the branch 4-webcontainer-api.

Last updated on

On this page