Val Town Runtime v3 – My mistakes were easy, the solutions simple

Val Town Runtime v3 – My mistakes were easy, the solutions simple

Date
August 18, 2023
Author
Steven Krouse

I’ve made many mistakes at Val Town, but my biggest and most frequent were making things superficially easy with hidden complexity. This is the story of how I fought JavaScript, and JavaScript won.

Val Town is a website to write and run code, so these easy features make for cool demos, but frustrating debugging, and a long list of gotchas you have to learn. We are in the process of untangling these mistakes, and replacing them with simple features, made easy, in Rich Hickey’s sense, where simple means that concepts aren’t entangled, and easy means that things are near at hand.

Long story short: don’t add custom features to JavaScript. Make standard JavaScript easier through editor features.

@ imports

I started Val Town with the dream of making programming feel like tweeting posting on 𝕏. For example, you import code like you tag someone on Twitter 𝕏: @stevekrouse.fetchJSON resolves to the value of fetchJSON in my namespace.

This is one of our best and most beloved features. It also causes problems:

  1. The @ symbol is not a valid JavaScript identifier character. This is a problem because folks want to run their Val Town code in the browser, locally, and other cloud platforms. It prevents your vals from being maximally useful, and it creates an artificial lock-in to our platform.
  2. All of our editor tooling that works on JavaScript (syntax highlighter, linter, language server) doesn’t work out of the box — we have to maintain a number of custom fixes and forks to get all of these things to (mostly) work. For example, our syntax highlighter breaks when you use a combination of backticks ` and the @ symbol.
  3. We had to build our own JavaScript bundler, to bundle your @ references in before we evaluate your code. Bundlers are hard (caching, scoping, resolving, versions), and the JavaScript world already has a number of better solutions. Deno handles imports beautifully all on its own — wouldn’t it be nice if we could use it to handle Val Town imports as well?
  4. The @ symbol is another thing to learn. People who already know JavaScript don’t want to learn special features, and people who are still learning JavaScript are doubly confused by custom syntax, because they can’t tell the difference between Val Town JavaScript and normal JavaScript.
  5. We want vals to be embedded all over the internet to explain APIs, JavaScript features, programming concepts, and it’s confusing for a code embed to use nonstandard JavaScript.

The crux of the issue is that we did too much: we added to custom syntax and built our own import scheme. The solution is to remove our magic, and make it easy to do things the simple way.

The future of Val Town imports are URL imports and an import map:

import { fetchJSON } from "vt/stevekrouse/fetchJSON"

fetchJSON("https://api.example.com")

This code will run unchanged in the browser, Deno, and Node with an experimental flag.

We’ll then make it easy by still mapping the @ symbol for inline lookup and completion. The difference is when you accept the completion: it‘ll complete to just the name of the val, not the user’s handle or the @ symbol. It will automatically add the static import for you at the top, like other IDEs do.

This new scheme will also make version pinning much simpler. We never felt good about shoving it into our @ imports because it just felt like more magic: @stevekrouse.foo_v1 relies on weird parsing and disallows you from ending your own variables like that. Now we can put the version numbers in the import URLs, which is a standard thing in the Deno world already.

import { fetchJSON } from "vt/stevekrouse/fetchJSON@v10"

Import maps

An import map allows us to make these imports a bit easier: vt/ instead of https://raw.val.town/.

{
  "imports": {
    "vt": "https://raw.val.town/"
  }
}

We have a couple ideas for authentication to Val Town, but my personal favorite is throwing it inside the import map via Basic Auth:

{
  "imports": {
    "vt": "https://username:token@raw.val.town/"
  }
}

This lets you import a private val the same way you import a public val. We’re also considering taking this authentication piece a step further, and returning our standard library functions pre-authenticated for you.

console.email

For example, let’s take another one of our favorite, Val Town features: console.email. It lets you email yourself as easily as you’d log to the console. For example, you could accept an HTML form submission and use console.email to forward the data to your email:

let formHandler = (req: Request) => {
  let data = Object.fromEntries((await req.formData()).entries())
  console.email(data, "New Form Submission");
}

It also hides complexity. For one, it pretends to be synchronous (like console.log), but it is not. Worse, it adds a custom function to the global console object, which means that it would not work well in other JavaScript runtimes.

We think the future of console.email looks like:

import { login } from 'vt/std/login'
import { email } from 'vt/std/email'

login('your val town auth token here')

await email({text: 'hello!', to: 'you@email.com'})

This is certainly simpler and more explicit, but it’s certainly not easy. Who wants to type all that? I think we can make it easier!

First let’s improve the authentication piece like I allude to above. If we add our authentication details in the import map, then we could dynamically generate the code we send back to you to include the very token you just sent to authenticate with us!

// https://username:token@raw.val.town/std/email

import { login } from 'vt/std/login'

login('token')

export let email = (data) => { /* ... */ } 

This way the future of console.email could be:

import { email } from 'vt/std/email'

await email({text: 'hello!', to: 'you@email.com'})

If you wanted a handy emailMe function with the to field pre-filled to point to you, you could fork this helper function to your account:

import { email } from 'vt/std/email'

export let emailMe = text => await email({text, to: 'you@email.com'})

And use it like this:

import { emailMe } from 'vt/yourUsername/emailMe'

await emailMe("hello me!")

We are considering adding this emailMe helper function to everyone’s account by default. This way we can easily support your pre-existing muscle memory: when you type console.email, it’d complete to emailMe and import it for you automatically.

If you want to live in the future, you can play with @std.email today! It’s currently only available for Val Town Pro members to limit the potential for abuse, because it can email anyone (console.email can just email yourself).

Runtime v3

Our highest priority now is working on our Runtime v3 that will undo these easy mistakes, and a couple others. We are committed to a smooth transition, but aren’t totally sure how it will look yet. One option would be to maintain two separate runtimes indefinitely. My personal preference is a transpiler from Val Town v2 code to standard JavaScript that can run anywhere, including our v3 runtime.

As always, we are very eager for feedback and suggestions. If you can think of even simpler solutions to some of these problems that we haven’t uncovered yet, please send them our way or join the discussion in our Discord.