Web - TypeScript and Svelte (Parcel)

These are the steps to set up and run a simple TypeScript Web app that calls into a shared core.

Note

This walk-through assumes you have already added the shared and shared_types libraries to your repo, as described in Shared core and types.

Info

There are many frameworks available for writing Web applications with JavaScript/TypeScript. We've chosen Svelte with Parcel for this walk-through. However, a similar setup would work for other frameworks.

Create a Svelte App

Let's create a new project which we'll call web-svelte:

mkdir web-svelte
cd web-svelte
mkdir src/

Compile our Rust shared library

When we build our app, we also want to compile the Rust core to WebAssembly so that it can be referenced from our code.

To do this, we'll use wasm-pack, which you can install like this:

# with homebrew
brew install wasm-pack

# or directly
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

Now that we have wasm-pack installed, we can build our shared library to WebAssembly for the browser.

(cd shared && wasm-pack build --target web)

Create a package.json file and add the wasm:build script:

"scripts": {
    "wasm:build": "cd ../shared && wasm-pack build --target web",
    "start": "npm run build && concurrently -k \"parcel serve src/index.html --port 8080 --hmr-port 1174\" ",
    "build": "pnpm run wasm:build && parcel build src/index.html",
    "dev": "pnpm run wasm:build && parcel build src/index.html"
  },

Also make sure to add the shared and shared_types as local dependencies to the package.json:

  "dependencies": {
    // ...
    "shared": "file:../shared/pkg",
    "shared_types": "file:../shared_types/generated/typescript"
    // ...
  }

Create an app to render the UI

Create a main.ts file in src/:

import "reflect-metadata";

import App from "./App.svelte";

document.body.setAttribute("data-app-container", "");

export default new App({ target: document.body });

This file is the main entry point which instantiates a new App object. The App object is defined in the App.svelte file:

<script lang="ts">
  import "bulma/css/bulma.css";
  import { onMount } from "svelte";
  import { update } from "./core";
  import view from "./core";
  import {
    EventVariantReset,
    EventVariantIncrement,
    EventVariantDecrement,
  } from "shared_types/types/shared_types";

  onMount(async () => {
    console.log("mount");
  });
</script>

<section class="box container has-text-centered m-5">
  <p class="is-size-5">{$view.count}</p>
  <div class="buttons section is-centered">
    <button
      class="button is-primary is-danger"
      on:click={() => update(new EventVariantReset())}
    >
      {"Reset"}
    </button>
    <button
      class="button is-primary is-success"
      on:click={() => update(new EventVariantIncrement())}
    >
      {"Increment"}
    </button>
    <button
      class="button is-primary is-warning"
      on:click={() => update(new EventVariantDecrement())}
    >
      {"Decrement"}
    </button>
  </div>
</section>

This file implements the UI and the behaviour for various user actions.

In order to serve the Svelte app, create a index.html in src/:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5" />
  <title>Simple Counter</title>
  <meta name="apple-mobile-web-app-title" content="Simple Counter" />
  <meta name="application-name" content="Simple Counter" />
</head>
<body>
  <script type="module" src="main.ts"></script>
</body>
</html>

This file ensures that the main entry point gets called.

Wrap the core to support capabilities

Let's add a file src/core.ts which will wrap our core and handle the capabilities that we are using.

import { process_event, view } from "shared";
import initCore from "shared";
import { writable } from "svelte/store";
import {
  EffectVariantRender,
  ViewModel,
  Request,
} from "shared_types/types/shared_types";
import type { Effect, Event } from "shared_types/types/shared_types";
import {
  BincodeSerializer,
  BincodeDeserializer,
} from "shared_types/bincode/mod";

const { subscribe, set } = writable(new ViewModel("0"));

export async function update(event: Event) {
  console.log("event", event);
  await initCore();

  const serializer = new BincodeSerializer();
  event.serialize(serializer);

  const effects = process_event(serializer.getBytes());
  const requests = deserializeRequests(effects);
  for (const { id, effect } of requests) {
    processEffect(id, effect);
  }
}

function processEffect(_id: number, effect: Effect) {
  console.log("effect", effect);
  switch (effect.constructor) {
    case EffectVariantRender: {
      set(deserializeView(view()));
      break;
    }
  }
}

function deserializeRequests(bytes: Uint8Array): Request[] {
  const deserializer = new BincodeDeserializer(bytes);
  const len = deserializer.deserializeLen();
  const requests: Request[] = [];
  for (let i = 0; i < len; i++) {
    const request = Request.deserialize(deserializer);
    requests.push(request);
  }
  return requests;
}

function deserializeView(bytes: Uint8Array): ViewModel {
  return ViewModel.deserialize(new BincodeDeserializer(bytes));
}

export default {
  subscribe,
};

This code sends our (UI-generated) events to the core, and handles any effects that the core asks for via the update() function. Notice that we are creating a store to update and manage the view model. Whenever update() gets called to send an event to the core, we are fetching the updated view model via view() and are updating the value in the store. Svelte components can import and use the store values.

Notice that we have to serialize and deserialize the data that we pass between the core and the shell. This is because the core is running in a separate WebAssembly instance, and so we can't just pass the data directly.

Build and serve our app

We can build our app, and serve it for the browser, in one simple step.

npm start

Success

Your app should look like this:

simple counter app