> ## Documentation Index
> Fetch the complete documentation index at: https://knitkit.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Server-Side Rendering with knitkit Node Loader

> Use @knitkit/node to load federated modules in Node.js via module.register loader hooks, with SRI verification and hydration-parity import maps.

`@knitkit/node` brings the same manifest-driven federation to your Node.js server. It installs `module.register` loader hooks so that bare specifiers you negotiate in an import map resolve to remote ESM modules fetched over HTTP — exactly the same modules the browser loads. Before executing any remote file, the loader verifies its `sha384` integrity hash. After rendering, `serializeImportMap` emits a `<script type="importmap">` tag with browser URLs so the client hydrates against the same dependency versions the server used — no dual-React, no hydration mismatch.

## Install

```bash theme={null}
npm i @knitkit/node
```

## How it works

`registerFederation(importMap)` calls Node's built-in `module.register` to install a pair of loader hooks (`resolve` + `load`) on a worker thread. From that point forward, any `import()` call whose specifier appears in the map's `"imports"` object resolves through the map — just like the browser does. Relative imports inside a remote module resolve against the remote's own URL, so the remote's full dependency graph loads over the network automatically.

The import map you pass to `registerFederation` is typically the one produced by `negotiateShared` (re-exported from `@knitkit/node`), which means the server and browser negotiate from the same manifest and arrive at the same winner for every shared package.

<Warning>
  Call `registerFederation` before importing any module that must resolve through the map. Node's loader hooks take effect for imports made **after** the call returns; earlier imports are already resolved and cannot be redirected.
</Warning>

## Step-by-step setup

<Steps>
  ### Negotiate the import map

  Fetch the remote manifest and run `negotiateShared` to pick winning versions for every shared dependency. `negotiateShared` is re-exported from `@knitkit/node` so you never need to import from two packages.

  ```js theme={null}
  import { negotiateShared } from "@knitkit/node";
  import { createRequire } from "node:module";
  import { pathToFileURL } from "node:url";

  // Resolve the local React file URL so the server import map
  // points react → the same copy react-dom/server uses.
  const require = createRequire(import.meta.url);
  const reactFile = pathToFileURL(require.resolve("react")).href;

  const manifestUrl = "https://cdn.example.com/widgets/knit.manifest.json";
  const res = await fetch(manifestUrl);
  const manifest = await res.json();

  const result = negotiateShared(
    [{ name: "widgets", manifest, baseUrl: res.url }],
    {
      react: {
        version: "18.3.1",
        requiredVersion: "^18.0.0",
        singleton: true,
        url: reactFile,           // local file URL for the server
      },
    },
  );
  // result.importMap is ready to give to registerFederation.
  ```

  ### Install the loader hooks

  Pass the negotiated import map to `registerFederation`. Do this at the very top of your server entry, before any federated import.

  ```js theme={null}
  import { registerFederation } from "@knitkit/node";

  registerFederation(result.importMap);
  // The hooks are now active. Any import() that matches the map
  // will be fetched over HTTP and SRI-verified before execution.
  ```

  ### Import your remote modules

  After `registerFederation` returns, use ordinary dynamic `import()`. The bare specifiers resolve through the map just as they would in the browser.

  ```js theme={null}
  // "widgets/Greeting" is in the map's "imports" — the hook intercepts
  // this import, fetches the remote ESM, verifies its integrity hash,
  // and returns the source for Node to evaluate.
  const Greeting = (await import("widgets/Greeting")).default;

  // Use it with react-dom/server as normal.
  import { renderToString } from "react-dom/server";
  import { createElement } from "react";

  const html = renderToString(createElement(Greeting, { name: "world" }));
  ```

  ### Serialize the import map for the browser

  Pass the **browser** import map (with CDN URLs rather than local file paths) to `serializeImportMap`. Embed the returned `<script>` tag in the `<head>` before any module script so the browser uses the same dependency versions — eliminating hydration mismatches.

  ```js theme={null}
  import { serializeImportMap } from "@knitkit/node";

  // Build a browser-facing map: same package names, CDN URLs.
  const browserMap = {
    imports: {
      react:              "https://esm.sh/react@18.3.1",
      "react-dom":        "https://esm.sh/react-dom@18.3.1",
      "react-dom/client": "https://esm.sh/react-dom@18.3.1/client",
      "widgets/Greeting": remoteModuleUrl,
    },
  };

  const importMapTag = serializeImportMap(browserMap);
  // Returns: <script type="importmap">{"imports":{...}}</script>

  const page = `<!doctype html>
  <html lang="en">
  <head>
    <meta charset="utf-8" />
    ${importMapTag}
  </head>
  <body>
    <div id="root">${html}</div>
    <script type="module">
      import { hydrateRoot } from "react-dom/client";
      import { createElement } from "react";
      const Greeting = (await import("widgets/Greeting")).default;
      hydrateRoot(document.getElementById("root"), createElement(Greeting, { name: "world" }));
    </script>
  </body>
  </html>`;
  ```
</Steps>

## SRI verification

Every remote module the loader fetches is checked against its `sha384` integrity hash before Node evaluates it. The hash comes from the `integrity` field of the import map, keyed by the resolved URL (or the bare specifier). If the fetched content does not match, the loader throws with code `KNIT_ERR_SRI_MISMATCH` and refuses to execute the file.

```js theme={null}
// Pass integrity hashes alongside the import map.
registerFederation({
  imports: {
    "widgets/Greeting": "https://cdn.example.com/widgets/Greeting.js",
  },
  integrity: {
    "widgets/Greeting": "sha384-<base64hash>",
  },
});

// If the remote file has been tampered with, the import throws:
// Error: Integrity check failed for https://cdn.example.com/widgets/Greeting.js:
//   expected sha384-<base64hash>, got sha384-<actual>.
//   The remote asset does not match its pinned hash — refusing to execute it.
// error.code === "KNIT_ERR_SRI_MISMATCH"
```

`knitkit build` writes `sha384` hashes for every expose into `knit.manifest.json` automatically, so you get SRI protection without any manual hashing.

## Module cache

The loader hooks run on a Node worker thread and carry measurable per-import overhead. `ModuleCache` (exported from `@knitkit/node`) stores fetched and SRI-verified module source in memory keyed by resolved URL, so repeated SSR render paths pay the network and verification cost only once per process lifetime.

```js theme={null}
import { ModuleCache } from "@knitkit/node";

const cache = new ModuleCache();
// cache.get(url)    — retrieve a cached entry
// cache.set(url, { source, format: "module", integrity })
// cache.has(url)
// cache.delete(url)
// cache.clear()
// cache.size
```

The hooks bundle their own internal `ModuleCache` instance automatically. Export your own instance only when you need to pre-warm, introspect, or selectively invalidate cache entries.

## Full server entry example

The following is drawn from the `examples/node-ssr` example in the knitkit repo. It shows the complete flow: fetch manifest, install hooks, render with `react-dom/server`, serialize the import map, and return HTML.

```js theme={null}
// server/render.mjs
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import { createElement } from "react";
import { renderToString } from "react-dom/server";
import { registerFederation, serializeImportMap } from "@knitkit/node";

const require = createRequire(import.meta.url);
const reactFile = pathToFileURL(require.resolve("react")).href;

export async function setupRenderer(remoteBase) {
  const manifestUrl = `${remoteBase}/knit.manifest.json`;
  const res = await fetch(manifestUrl);
  if (!res.ok) throw new Error(`Failed to fetch manifest: HTTP ${res.status}`);
  const manifest = await res.json();
  const baseUrl = res.url;

  const decl = manifest.exposes["./Greeting"];
  const remoteUrl = new URL(decl.url, baseUrl).href;

  // Server map: react → local file (shares the instance with react-dom/server).
  // The remote module is SRI-pinned via the integrity field.
  registerFederation({
    imports: { react: reactFile, "widgets/Greeting": remoteUrl },
    integrity: decl.integrity ? { "widgets/Greeting": decl.integrity } : undefined,
  });

  const Greeting = (await import("widgets/Greeting")).default;

  // Browser map: CDN URLs so the client hydrates with the same versions.
  const head = serializeImportMap({
    imports: {
      react:              "https://esm.sh/react@18.3.1",
      "react-dom":        "https://esm.sh/react-dom@18.3.1",
      "react-dom/client": "https://esm.sh/react-dom@18.3.1/client",
      "widgets/Greeting": remoteUrl,
    },
  });

  function renderHtml(props = { name: "world" }) {
    const appHtml = renderToString(createElement(Greeting, props));
    return `<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>knitkit — SSR</title>
  ${head}
</head>
<body>
  <div id="root">${appHtml}</div>
  <script type="module">
    import { hydrateRoot } from "react-dom/client";
    import { createElement } from "react";
    const Greeting = (await import("widgets/Greeting")).default;
    hydrateRoot(
      document.getElementById("root"),
      createElement(Greeting, ${JSON.stringify(props)}),
    );
  </script>
</body>
</html>`;
  }

  return { renderHtml };
}
```

<Note>
  `renderToString` is used here for simplicity. `react-dom/server`'s streaming APIs (`renderToPipeableStream`, `renderToReadableStream`) drop in the same way — `registerFederation` and `serializeImportMap` are rendering-strategy agnostic.
</Note>

## Programmatic vs. `--import` bootstrap

If you prefer not to call `registerFederation` in code, you can supply the import map via environment variable and use the `--import` flag:

```bash theme={null}
FEDKIT_IMPORT_MAP_JSON='{"imports":{"react":"file:///...","widgets/Greeting":"https://..."}}' \
  node --import @knitkit/node/register server.mjs
```

`@knitkit/node/register` reads `FEDKIT_IMPORT_MAP_JSON` (inline JSON) or `FEDKIT_IMPORT_MAP` (a path to a JSON file) and calls `registerFederation` before your server entry runs. If neither variable is set, it emits a process warning and skips installation.
