JavaScript Runner

The JavaScript runner treats QIP work as checked content components: declare what goes in, declare what comes out, and then call the result like an ordinary function.

Contracts #

Use contentTypeUTF8 or contentTypeBytes to create a frozen content boundary. Pass a MIME type when the component requires a more specific contract.

import { contentTypeBytes, contentTypeUTF8 } from "/qip-runner.js";

const text = contentTypeUTF8();
const markdown = contentTypeUTF8("text/markdown");
const html = contentTypeUTF8("text/html");
const bmp = contentTypeBytes("image/bmp");

Encoding follows one subtype rule: UTF-8 may safely widen to raw bytes, with the runner encoding the JavaScript string before calling the bytes component. Raw bytes never implicitly narrow to UTF-8. Content-type matching is directional: a component with a declared input MIME type requires an exact match, while an unspecified component input is generic and accepts the current type. An unspecified current type never satisfies a declared input type.

Generic output preserves the current MIME type except when raw bytes are converted to UTF-8, which produces text with an unspecified MIME type. These are the same planning rules used by qip run and qip dry run. Planning uses only the declared contracts, not sample values or browser-specific coercion, so the result is deterministic for the same recipe description.

Content types are not normalized. When present, they must already be lowercase media types without parameters or whitespace, such as text/html or image/bmp. Hosts compare them exactly.

Components #

contentComponent(input, implementation, output) returns a callable component with .input and .output metadata.

The implementation can be a WebAssembly.Module:

const markdownToHtml = contentComponent(markdown, markdownModule, html);
const fragment = markdownToHtml("# Hello");

For Wasm modules, the runner validates the declared QIP exports up front:

If the module declares content-type exports, they must match the supplied contracts.

The implementation can also be a JavaScript function:

const trim = contentComponent(text, (value) => value.trim(), text);

JavaScript functions are checked at the call boundary: UTF-8 contracts receive and return strings, byte contracts receive and return Uint8Array.

Recipes #

contentRecipe(input, components, output) composes content components and returns another callable component.

const page = contentRecipe(markdown, [markdownToHtml, htmlPageWrap], html);
const result = page("# qip");

Recipe creation checks the whole chain:

Recipes are components, so they can be nested:

const cleanMarkdown = contentRecipe(markdown, [trim], markdown);
const cleanPage = contentRecipe(markdown, [cleanMarkdown, markdownToHtml, htmlPageWrap], html);

An empty recipe is allowed only when input and output are compatible:

const identityText = contentRecipe(text, [], text);

Browser Example #

<script type="module">
  import {
    contentComponent,
    contentRecipe,
    contentTypeUTF8,
  } from "/qip-runner.js";

  const markdown = contentTypeUTF8("text/markdown");
  const html = contentTypeUTF8("text/html");

  const markdownModule = await WebAssembly.compileStreaming(
    fetch("/components/text/markdown/commonmark.0.31.2.wasm"),
  );
  const pageModule = await WebAssembly.compileStreaming(
    fetch("/components/text/html/html-page-wrap.wasm"),
  );

  const markdownToHtml = contentComponent(markdown, markdownModule, html);
  const htmlPageWrap = contentComponent(html, pageModule, html);
  const renderPage = contentRecipe(markdown, [markdownToHtml, htmlPageWrap], html);

  console.log(renderPage("# qip"));
</script>

For Node, run from a package that treats .js files as ES modules, or import an .mjs copy of the runner.

import { readFile } from "node:fs/promises";
import {
  contentComponent,
  contentRecipe,
  contentTypeUTF8,
} from "./site/qip-runner.js";

const markdown = contentTypeUTF8("text/markdown");
const html = contentTypeUTF8("text/html");

const markdownModule = await WebAssembly.compile(
  await readFile("components/text/markdown/commonmark.0.31.2.wasm"),
);
const pageModule = await WebAssembly.compile(
  await readFile("components/text/html/html-page-wrap.wasm"),
);

const markdownToHtml = contentComponent(markdown, markdownModule, html);
const htmlPageWrap = contentComponent(html, pageModule, html);
const renderPage = contentRecipe(markdown, [markdownToHtml, htmlPageWrap], html);

console.log(renderPage("# qip"));