The Technology Behind “Moyuk”: Create, Run and Share Tools with TypeScript on Your Browser
Tags: typescript, nextjs, supabase, sandbox
Published: 2023-04-10 11:38:27


After over a year of development, I launched my personal project, [Moyuk, on Product Hunt](https://www.producthunt.com/posts/moyuk-beta). In this post, I'll share some technical insights and knowledges about the service.

## What I Created

### Moyuk

{% embed https://moyukapp.com %}

You can check out the Product Hunt launch [here](https://www.producthunt.com/posts/moyuk-beta) 🚀:


#### Brief Overview

**Simplest way to create and share your custom tools**

In short, Moyuk is a platform that turns functions written in TypeScript into web applications (or "Apps") that can be executed, managed, and shared directly from your browser.

Please check out the link below for more information:

{% embed https://medium.com/@kohii/introducing-moyuk-a-service-for-creating-running-and-sharing-tools-in-seconds-on-your-browser-7067ca513936 %}

## Overview of Technical Elements

In addition to the common application components (such as data input/output and UI rendering), Moyuk has several distinctive features:

- Components that analyze and transpile user-written TypeScript
- A sandboxed JavaScript execution environment for running transpiled code

These elements are all written in TypeScript and organized as a monorepo.

### Main Technology Stack

- Application
    - [TypeScript](https://www.typescriptlang.org/)
    - [React](https://react.dev/)
    - [Next.js](https://nextjs.org/) (Application framework)
    - [MUI](https://mui.com/) (UI library)
    - [tRPC](https://trpc.io/) (Frontend-backend communication)
    - [Prisma](https://www.prisma.io/) (ORM)
- Infrastructure
    - [Vercel](https://vercel.com/)
    - [Supabase](https://supabase.com/) (DB, authentication, storage)
    - [Cloudflare](https://www.cloudflare.com/) (Workers, Pages)
- Other
    - [Docusaurus](https://docusaurus.io/) (Documentation)
    - [Turborepo](https://turbo.build/) (Monorepo)

### Configuration

![Configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3113atvzyjagnj2ucm7o.png)


- The core application is built on Next.js
    - Deployed on Vercel, with Supabase used for DB, authentication, and storage
- A sandboxed JavaScript execution environment runs within the browser to execute user-written code
    - Content for the sandbox is served from Cloudflare

## Moyuk as a General Application

Moyuk is built on Next.js and deployed on Vercel.

### Features Directory

One of the distinctive aspects of the project is the adoption of the [features directory](https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md), inspired by [Bulletproof React](https://github.com/alan2207/bulletproof-react). I personally prefer organizing components with high functional cohesion close together, which is why I chose this structure. I try not to write too much logic inside the `pages` directory of Next.js and instead just combine and use components exported from `features`.

### State Management

After considering various options, I decided not to use a library for global state management.

Initially, I used [zustand](https://github.com/pmndrs/zustand) as a lightweight state management library, but eventually replaced it with React's Context, which was deemed sufficient. This decision was based on a policy of minimizing global state, which has not caused much pain.

Generally, the [React Query](https://react-query-v3.tanstack.com/) client manages data fetched from the server.
Other data states are handled within appropriate components or hooks.

### Backend

The database is [Supabase](https://supabase.com/), with [Prisma](https://www.prisma.io/) as the ORM, and [tRPC](https://trpc.io/) for frontend-backend data exchange. tRPC endpoints are built on top of Next.js API Routes and deployed as Vercel Serverless Functions. Supabase is used as a managed PostgreSQL service.

Supabase is a BaaS "Firebase alternative," allowing direct data reads and writes from the frontend using a client library. Initially, I used this system but switched to tRPC + Prisma as it became difficult to handle complex configurations and store domain logic in stored procedures.

The combination of tRPC + Prisma offers a great developer experience.

### UI

I'm using [MUI](https://mui.com/) as the component library. The richness of MUI components greatly helped in quickly building the UI.

Forms are built with the popular [React Hook Form](https://react-hook-form.com/) + [Zod](https://github.com/colinhacks/zod).

### Page Rendering

Next.js offers various rendering methods, which are crucial for performance and Web Vitals scores.

#### Basic Policy

The policy for Moyuk is as follows:

- Static pages (e.g., landing pages) → SG
    - Generated statically at build time and cached on the CDN for speed.
- Authenticated dynamic pages (e.g., dashboard) → CSR
    - Data is fetched client-side.
- Public dynamic pages (e.g., App or profile) → ISR
    - Generated server-side (& cached on CDN), with revalidation on background requests when content becomes outdated.
    - When page data is updated, cache is explicitly purged using [On-demand revalidation](https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration#on-demand-revalidation).

#### tRPC and Rendering

tRPC offers [SSR features](https://trpc.io/docs/nextjs/ssr), which enables automatic SSR for all pages. Initially, I used it without thinking but later stopped due to performance issues. Instead, I began explicitly prefetching the required data using [Server-Side Helpers](https://trpc.io/docs/nextjs/server-side-helpers).

#### Rendering Pages with User-Selectable Private/Public Settings

Users can choose to make their created App public or private. If all App pages are rendered with ISR without consideration, CDN caches for private App pages will be created. Simply marking private App pages as 404 (`notFound: true`) will return a 404 to the App owner.

To resolve this, in Moyuk, `getStaticProps` fetches the App data. If the App is public, it renders normally with the data. For private (or non-existent) Apps, it renders a page with empty content. When users access the page, the App data is fetched again client-side. If the viewer is the App owner, the page continues to render. If not, a Not Found page is displayed.

## Generating an App from User-Written TypeScript

Below is the UI for the App editing screen. When you write code in TypeScript, a preview of the App is displayed on the right side.


![App editor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vn7o2nsa8lfug5t639v4.png)

There are mainly two routes through which the user-written TypeScript is processed, and the App preview is generated.

### 1. Extracting Type Information from TypeScript Functions

In Moyuk, a form is automatically generated from the type information of the `export default` function.

The extraction of type information is done using the [TypeScript Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API). It was quite challenging due to the lack of documentation and articles.

#### Helpful Resources

- [TypeScript Deep Dive](https://basarat.gitbook.io/typescript/overview) - This resource helped in understanding internal concepts like the Scanner, Checker, and Binder.
- [Using the Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) - Sample code for specific use cases is provided, which helped in guessing the purpose of each API.
- [TypeScript AST Viewer](https://ts-ast-viewer.com/) - This viewer was useful in guessing the internal concepts and behavior of the Compiler API.

#### Support for Deno-compatible import statements

In Moyuk, you can use Deno-compatible import statements.

```tsx
import { format } from "https://deno.land/std@0.181.0/fmt/duration.ts";
import { encode } from "npm:js-base64@^3.7";
```

The TypeScript Compiler itself does not have a mechanism for automatically resolving remote modules, so compiling them as it will result in errors. I will explain how this is resolved.

#### https

First, to resolve imports starting with `https://`, all import URLs in the code are extracted before calling the TS Compiler API, and the files they reference are downloaded in advance.

- Files referenced by the downloaded files are also downloaded in a cascading manner.
- If the reference destination supports the [X-TypeScript-Type header](https://deno.land/manual@v1.32.0/advanced/typescript/types#using-x-typescript-types-header) on a CDN (e.g., esm.sh, Skypack), the corresponding `.d.ts` file is downloaded.

Next, the actual compiler is called, but with some modifications. The TS Compiler API has a component called `CompilerHost` that resolves the reference file for a module name. This is customized to resolve downloaded files for URL-formatted module names.

#### npm

By using the `npm:` prefix ([npm: specifiers](https://deno.land/manual/node/npm_specifiers)), you can import npm packages.

Moyuk uses a CDN called [esm.sh](http://esm.sh/) to download npm packages. esm.sh builds and distributes npm packages in ES Module format, so you can use npm packages in web-standard JS runtimes like browsers.

In Moyuk, if there is an import statement starting with `npm:`, it is converted to an esm.sh URL, and then treated in the same way as the aforementioned https case.

`npm:js-base64@^3.7` → `https://esm.sh/js-base64@^3.7`

### 2. Transpiling TypeScript to JavaScript

Since the TypeScript written by users is actually executed in the browser, it is transpiled to executable JavaScript. If an import statement references an external module, that module is bundled into a single JavaScript file along with the user-written TypeScript.

Internally, [esbuild](https://esbuild.github.io/) is used. Esbuild has a plugin system, and I created custom plugins to resolve import statements like `npm:` and `https://`.

By the way, during app editing and preview, these processes are executed within a Web worker. When you publish an app, the build is performed on the backend.

## Executing User-Written Code Safely

Moyuk needs to actually execute the code written by users and obtain the results.
The famous method for dynamically executing code in JavaScript is [eval](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval), but it [is not safe](https://www.notion.so/PH-First-comment-7e9cbbf8848b4db4af0ecb367da7e435).
Considering the possibility that app creators may write malicious code, it is necessary to execute code in an isolated, safe environment.

### Selecting a Sandbox Technology for the JS Runtime

After extensive research, it was difficult to decide because many options were not secure. The table below shows some promising technologies that were investigated.

| Name | Client / Server | Description | Notes |
| --- | --- | --- | --- |
| [QuickJS](https://bellard.org/quickjs/) | Client | A lightweight JS engine developed by Fabrice Bellard. Can be run as WASM when combined with [quickjs-emscripten](https://github.com/justjake/quickjs-emscripten). Figma plugins run on this. | Initially considered, but eventually abandoned because it was imperfect. |
| [WebContainers](https://webcontainers.io/) | Client | A browser-based Node.js developed by StackBlitz. More faithful reproduction of Node.js compared to Sandpack. | Not considered because it was discovered late in development. |
| [Sandpack](https://sandpack.codesandbox.io/) | Client | A browser-based Node.js developed by CodeSandbox. Runs on more browsers compared to WebContainers. | Not considered because it was discovered late in development. |
| [Deno Deploy Subhosting](https://deno.com/subhosting) | Server | A new Deno Deploy-related service. An environment prepared for safely executing user-input JS/TS on the Edge. | Discovered midway through development but not noticed until recently. Still in Private Beta, but a top candidate depending on pricing. |
| [ShadowRealm](https://github.com/tc39/proposal-shadowrealm) | Client | An API for creating separate Realms (similar to global scope). | Rejected due to many limitations and being in the proposal stage (Stage 3). |
| Custom runtime | Server | Setting up a custom server and creating a sandbox using Deno / Node.js or similar. | Rejected due to cost and maintenance concerns. |
| [iframe sandbox](https://www.w3schools.com/tags/att_iframe_sandbox.asp) | Client | An iframe with the sandbox attribute becomes sandboxed. | See below. |

In the end, a combination of iframe sandbox, web worker, and Content Security Policy (CSP) was adopted.
The iframe sandbox isolates the environment, CSP restricts contact with the outside world, and the web worker executes the code.

#### iframe

First, create the iframe dynamically. Add the [sandbox](https://www.w3schools.com/tags/att_iframe_sandbox.asp) attribute to the iframe and serve the content loaded into the iframe from a separate domain (different origin) from [moyukapp.com](http://moyukapp.com/). This prevents any access from inside the iframe to the outside. Note that [CodeSandbox](https://codesandbox.io/) also uses an iframe, but Moyuk's settings are much stricter.

```ts
const iframe = document.createElement("iframe");
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
iframe.style.display = "none";
iframe.src = sandboxUrl;
document.body.appendChild(iframe);
```

Please note that it is dangerous to add `allow-scripts` and `allow-same-origin` to the `sandbox` attribute and fetch iframe content from the same origin, as this will break the sandboxing.

#### Web worker and CSP

Next, start a Web worker inside the iframe. The JS loaded into the Web worker has the following [Content-Security-Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) specified in the response header:

```
Content-Security-Policy: default-src 'none'; script-src blob:
```

This prevents scripts loaded into the Web worker from executing network access, `eval`, and other operations. Loading scripts from `blob:` URLs to execute user-written code is allowed (explained later).

In Moyuk, network access can be explicitly allowed by the user (Cloudflare Workers are used to dynamically rewrite the CSP).


![You can explicitly allow network access](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p4ffl1hv8vkc0b0qh9k4.png)

#### Loading user-written code

Load the user-written code (transpiled to JS) into the aforementioned Web worker.

Moyuk's main application passes the code to the iframe, which in turn passes it to the Web worker. The Web worker converts the received code into a `blob:` URL and performs a dynamic import. The return value of the dynamic import allows you to obtain members exported from the user-written script.

```tsx
class ExecutionContext {
  async importSourceCode(code: string) {
    const url = URL.createObjectURL(new Blob([code], { type: "text/javascript" }));
    this.exportedMembers = await import(url);
  }

  ...
}
```

Scripts imported via `blob:` URLs inherit the parent (Web worker)'s CSP, so they cannot perform external network access or execute `eval`.

Note that in Firefox, dynamic import within a Web worker is not yet supported, making it an unsupported browser for Moyuk.

#### Executing user-written functions

Execute the target function with the values entered on the App's UI as arguments. The process looks something like this:

```tsx
class ExecutionContext {
  ...

  async callFunction(functionName: string, args: unknown[]) {
    const f = this.exportedMembers[functionName];
    if (f && typeof f === "function") {
      const returnValue = f(...args);
      const value = await Promise.resolve(returnValue);
      return {
        type: "SUCCESS",
        value,
      };
    }
    return {
      type: "FUNCTION_NOT_FOUND",
    };
  }
}
```

Search for the function to be executed from the members `export`ed from the code dynamically imported in the previous step, provide the arguments, and execute the function. If the result is a `Promise`, `await` and return the result.

## Conclusion

Thank you for reading until the end!
As the content is quite partial, if you have any questions, please feel free to ask.

I'm also planning to write about personal development retrospectives and lessons learned at some point.

And please give Moyuk a try!
[https://moyukapp.com/](https://moyukapp.com/)

I appreciate your feedback and am happy to answer any questions you may have. The following channels are available:

- Send a DM/reply to [@moyukapp](https://twitter.com/moyukapp) on Twitter
- Create an Issue in Moyuk's [GitHub repository](https://github.com/moyukapp/moyuk)
- Send an email to [hello@moyukapp.com](mailto:hello@moyukapp.com)

Upvotes and comments on Product Hunt are also greatly appreciated and motivating 🙏

## Resources

Moyuk onProduct Hunt:
{% embed https://www.producthunt.com/posts/moyuk-beta %}

Introduction to Moyuk:
{% embed https://medium.com/@kohii/introducing-moyuk-a-service-for-creating-running-and-sharing-tools-in-seconds-on-your-browser-7067ca513936 %}

Docs:
https://docs.moyukapp.com/