Ultra: The Quest for Zero-Legacy

📖 Philosophy

Ultra takes a non-prescriptive approach to web-app development. You can configure it to use most existing libraries that you are accustomed to — or you can write your own...

We want Ultra to do a few things, and do them well.

  • React Streaming SSR, with Suspense support
  • Native import maps from top to bottom
  • Import maps with vendored dependencies in production
  • Shipping pure, unbundled ESM always
  • Simplifying your tool chain, removing the 'black-box bundler' approach
  • Write apps that work the same way in the browser that they do on the server
  • Utilise service workers to cache your ESM source code

Give us ESM or give us death


Breakdown of a basic Ultra project

To follow along at home, run this command to quickly scaffold out a basic Ultra project.

deno run -A -r https://deno.land/x/ultra/create.ts

importMap.json

{
  "imports": {
    "react": "https://esm.sh/react@18.2.0",
    "react/": "https://esm.sh/react@18.2.0/",
    "react-dom": "https://esm.sh/react-dom@18.2.0",
    "react-dom/": "https://esm.sh/react-dom@18.2.0/",
    "ultra/": "https://deno.land/x/ultra@v2.0.0-alpha.6/"
  }
}

Atm, these are the only deps required to run an Ultra project. Simple, I like it.

server.tsx

import { serve } from "https://deno.land/std@0.176.0/http/server.ts";
import { createServer } from "ultra/server.ts";
import App from "./src/app.tsx";

const server = await createServer({
  importMapPath: import.meta.resolve("./importMap.json"),
  browserEntrypoint: import.meta.resolve("./client.tsx"),
});

server.get("*", async (context) => {
  /**
   * Render the request
   */
  const result = await server.render(<App />);

  return context.body(result, 200, {
    "content-type": "text/html",
  });
});

serve(server.fetch);

This file controls how your app will render on the server. It's using Deno's std http server, you can probably use another one if you want.

createServer kickstarts the Ultra renderer and static asset pipeline, it only needs your import map and client entry point.

You can also look at creating API routes here by following this example.

client.tsx

import hydrate from "ultra/hydrate.js";
import App from "./src/app.tsx";

hydrate(document, <App />);  

This should look familiar to most... This is your client entrypoint, and what is used for client rendering. It can be customised if needed.

src/

Put your source code here.

public/

Static files go here. When building for production, these files will be versioned.

deno.json

{
  "tasks": {
    "dev": "deno run -A --no-check --watch ./server.tsx",
    "build": "deno run -A ./build.ts",
    "start": "ULTRA_MODE=production deno run -A --no-remote ./server.js"
  },
  "compilerOptions": {
    "jsx": "react-jsxdev",
    "jsxImportSource": "react"
  },
  "importMap": "./importMap.json"
}

We use Deno's native task runner/config file.

Try running deno task dev: This will spin up a development server and watch for file changes.

We use the react-jsx and react compiler options so you don't need to import React from 'react' everywhere.