Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-rwsdk-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@navita/vite-plugin": minor
---

Add rwsdk (RedwoodJS SDK) support via new `@navita/vite-plugin/rwsdk` export
88 changes: 88 additions & 0 deletions documentation/200-integrations/240-rwsdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
title: rwsdk
description:
---

# rwsdk

A plugin for integrating Navita with [rwsdk](https://rwsdk.com/) (RedwoodJS SDK).

> Navita has first-class support for rwsdk! It works with Cloudflare Workers SSR! 🎉

## Installation

```bash
npm install --save-dev @navita/vite-plugin
```

## Setup

Add the plugin to your Vite configuration, along with any desired [plugin configuration](#configuration).

```js
// vite.config.mts
import { defineConfig } from "vite";
import { redwood } from "rwsdk/vite";
import { cloudflare } from "@cloudflare/vite-plugin";
import { navitaRwsdk } from "@navita/vite-plugin/rwsdk";

export default defineConfig({
plugins: [
navitaRwsdk({
// configuration
}),
cloudflare({ viteEnvironment: { name: "worker" } }),
redwood(),
],
});
```

Please note that the import is different from the Vite plugin. The rwsdk plugin is based on, and uses the Vite plugin,
but we've added some extra functionality to make it better suited for rwsdk's build architecture.

### Using Navita styles

In your `Document.tsx`, add a link to the virtual Navita stylesheet:

```tsx
// src/Document.tsx
export const Document: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<html lang="en">
<head>
<meta charSet="utf-8" />
<link rel="stylesheet" href="/virtual:navita.css" />
</head>
<body>
<div id="root">{children}</div>
<script>import("/src/client.tsx")</script>
</body>
</html>
);
```

The plugin will automatically replace the virtual path with the hashed CSS file path during the build process.

## How it works

rwsdk has a unique build architecture with three passes:

1. **Client build**: Standard Vite client build that outputs assets to `dist/client/`
2. **Worker build**: Server-side worker bundle that renders React components
3. **Linker pass**: A special build pass that processes the worker bundle and replaces asset placeholders with hashed paths from the client manifest

The base Navita plugin works correctly for the client build - CSS is emitted and registered in the Vite manifest.
The rwsdk plugin adds a `renderChunk` hook that runs during the linker pass to replace `/virtual:navita.css` references
with the actual hashed filename from the client manifest.

## Configuration

> The rwsdk plugin is based on the Vite plugin, so you can use the same configuration options.

The plugin accepts the following as optional configuration:

#### `importMap?: { callee: string, source: string }[]`
*Default*: `import { importMap } from "@navita/css"`

ImportMap is a feature that allows you to extend the functionality of Navita.

If you provide your own, it will be merged with the default importMap.
5 changes: 5 additions & 0 deletions packages/vite-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
"import": "./dist/remix.mjs",
"require": "./dist/remix.cjs",
"types": "./src/remix.ts"
},
"./rwsdk": {
"import": "./dist/rwsdk.mjs",
"require": "./dist/rwsdk.cjs",
"types": "./src/rwsdk.ts"
}
},
"publishConfig": {
Expand Down
94 changes: 94 additions & 0 deletions packages/vite-plugin/src/rwsdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as fsp from "node:fs/promises";
import * as path from "node:path";
import type { Plugin } from "vite";
import type { Options } from "./index";
import { navita, VIRTUAL_MODULE_ID } from "./index";

export function navitaRwsdk(options?: Options): Plugin[] {
let projectRootDir: string;
let base: string;

const navitaPlugin = navita(options);

return [
navitaPlugin,
{
name: "navita-rwsdk",
enforce: "post",

configResolved(config) {
projectRootDir = config.root;
base = config.base;
},

async renderChunk(code) {
// Only run during the linker pass on the worker environment
// @ts-expect-error - environment exists at runtime in Vite 6+
const environmentName = this.environment?.name;

if (environmentName !== "worker" || process.env.RWSDK_BUILD_PASS !== "linker") {
return null;
}

// Read the client manifest to find the navita CSS path
const manifestPath = path.resolve(
projectRootDir,
"dist",
"client",
".vite",
"manifest.json"
);

let manifestContent: string;
try {
manifestContent = await fsp.readFile(manifestPath, "utf-8");
} catch {
console.warn("[navita-rwsdk] Could not read client manifest, skipping CSS replacement");
return null;
}

const manifest = JSON.parse(manifestContent) as Record<string, {
file: string;
css?: string[];
}>;

// Find the navita CSS file in the manifest
let navitaCssPath: string | null = null;

for (const [key, value] of Object.entries(manifest)) {
// Check if this is the navita CSS entry directly
if (key.includes("navita") && key.endsWith(".css")) {
navitaCssPath = (base || "/") + value.file;
break;
}
// Also check if it's referenced in the css array of any entry
if (value.css) {
for (const cssFile of value.css) {
if (cssFile.includes("navita")) {
navitaCssPath = (base || "/") + cssFile;
break;
}
}
if (navitaCssPath) break;
}
}

if (!navitaCssPath) {
console.warn("[navita-rwsdk] Could not find navita CSS in manifest");
return null;
}

// Replace virtual:navita.css references with the actual hashed path
let newCode = code;
newCode = newCode.replaceAll(`/${VIRTUAL_MODULE_ID}`, navitaCssPath);
newCode = newCode.replaceAll(VIRTUAL_MODULE_ID, navitaCssPath);

if (newCode !== code) {
return { code: newCode, map: null };
}

return null;
},
},
];
}
Loading