Karagöz

Process Tabs

The processTabs property of the sandbox is responsible for managing processes and terminals.

Automatic Bootstrapping

For simple use cases, directly managing processes won't be necessary. Karagöz Sandbox offers the bootstrap function which does the following:

  1. Kills the dependency installation process and the dev server if either of them is running.
  2. On first run: listen to URL change events in the preview iframe to emit the latest URL to the parent window. This is needed to show the current URL in address bar of the preview.
  3. Open a terminal window if allowed by the sandbox options.
  4. Install dependencies (using the package manager configured in the options).
  5. Start the dev server (also using the package manager configured in the options).
  6. On first run: watch the current working directory to re-bootstrap when a file changes that should trigger re-install (e.g. package.json).

Manual Process Management

You can opt out of automatic bootstrapping simply by not calling sandbox.bootstrap(), then you will have full control over which processes to run and in what order.

Spawning a Process

Whether you opt in or out of automatic bootstrapping, you can manually spawn processes by calling editorTabs.open().

sandbox.processTabsopen(
  // Unique id. As a convention, you can use the string presentation of the command to run.
  'npm install',
  
  // Optional label to show for the process tab. 
  // If not provided, the process tab ID is shown instead.
  'Install', 
     
  // Optional context.
  {
     // The only mandatory property. The command to spawn the process with.
     command: 'npm',
     // Additional arguemtns.
     arguments: ['install', '--frozen-lockfile'],
     
     // Flags to enable/disable certain UI features (all false by default).
     canRestart: false,
     canStop: false,
     isHidden: false,
     suppressClose: false,
     suppressInput: false,
     
     // When true, the process is treated as a terminal: 
     // some flags are ignored and the tab will be shown in the
     // terminals panel and not in the processes panel.
     isTerminal: false,
  },
)

The context object has some additional properties that are used internally. You can learn more about it in the API Reference.

If you provide an ID that already exists, then the respective process tab will be focused without spawning a new process.

Stopping a Process

You can stop a process either

  • by calling sandbox.processTabs.close(id) which kills the process and closes its respective tab,
  • or by calling sandbox.processTabs.kill(id) which kills the process, but keeps its respective tab visible so the user still has access to its output.

Restarting a Process

A process can be restarted by calling sandbox.processTabs.restart(id).

Also, processes that have canRestart: true in their context show a restart button to respawn the process through the UI.

Example: Manual Bootstrapping

Let's bring everything together and manually start processes.

In the demo below, now process is started automatically, rather we have buttons to:

  • Trigger dependency installation.
  • Start the development server.
  • Spawn a terminal.
  • Close the terminal, if open.

Here is the code to handle these processes:

const installDeps = async () => {
  await sandbox.processTabs.open('npm install', 'Install Dependencies', {
    command: 'npm',
    args: ['install'],
    suppressClose: true,
    suppressInput: true,
  })
  // we await to correctly set installationCompleted flag
  await sandbox.processTabs.findTab('npm install')?.context?.process?.exit
  installationCompleted.value = true
}

const startDevServer = () => {
  sandbox.processTabs.close('npm install')
  // No need to await here because nothing depends on finishing this process
  sandbox.processTabs.open('npm start', 'Dev Server', {
    command: 'npm',
    args: ['start'],
    canRestart: true,
    canStop: true,
    suppressInput: true,
  })
}

const openTerminal = () => {
  sandbox.processTabs.open('terminal', 'Terminal', {
    command: 'jsh',
    isTerminal: true,
  })
}

const closeTerminal = () => {
  sandbox.processTabs.close('terminal')
}

Full example code

Example: Using Generators

A good use case for manual bootstrapping (or running some processes before automatic bootstrapping) is when using generators (e.g. npm create, create-react-app...etc.).

In the example below we will generate a Vite app using the React template by running the following command:

npm create vite@latest my-react-app -- --template react

There are a few things to consider here:

  • Karagöz Sandbox expect the files to be in the current working directory, while generators (usually) create a new directory to store the generated files. Because of that, we will have to move all the files up to the CWD after generation and (optionally) remove the now empty directory created by the generator.
  • We should aim to eliminate the need for user input by providing the command options directly. It might not work in all cases, but it's recommended.
  • Some generators might perform unwanted steps that cannot be prevented (e.g. the Analog generator initialises a Git repository, which won't work in a WebContainer).

So our workflow for the example will be as follows:

  1. Spawn a process to run the npm create command shown above.
  2. Move all the generated files from ./my-react-app to ..
  3. Remove the ./my-react-app directory.
  4. Run the automatic bootstrapping to install dependencies and start the development server.

With that our onMounted hook would look something like this:

onMounted(async () => {
  // Ensure injected promise has been resolved
  const container = await boot
  
  // Mount shell script to perform extra steps before bootstrapping
  await container.mount({
    'prepare.sh': {
      file: {
        contents: [
          'mv ./my-react-app/* ./my-react-app/.* ./',
          'rmdir ./my-react-app',
          'rm prepare.sh',
        ].join('\n'),
      },
    },
  })
  
  // Set start script
  sandbox.setOption('process', (old) => ({
    ...old,
    commands: {
      ...old.commands,
      devServer: 'npm run dev',
    },
  }))
  
  // Generate react app
  await sandbox.processTabs.open('npm create', 'Generating React App', {
    command: 'npm',
    args: 'create vite@latest my-react-app -y -- --template react'.split(' '),
    suppressClose: true,
    suppressInput: true,
  })
  await sandbox.processTabs.findTab('npm create')?.context?.process?.exit
  sandbox.processTabs.close('npm create')
  
  // Perform remaining steps before bootstrapping
  await sandbox.processTabs.open('prepare', 'Prepare', {
    command: 'jsh',
    args: ['prepare.sh'],
    isHidden: true,
  })
  
  // Hand over to bootstrap()
  await sandbox.bootstrap()
  
  sandbox.editorTabs.open('./src/App.jsx')
})

Full example code