An open API service indexing awesome lists of open source software.

https://github.com/yusukebe/vite-ssr-components

JSX Components and Vite plugins for SSR apps
https://github.com/yusukebe/vite-ssr-components

Last synced: about 2 months ago
JSON representation

JSX Components and Vite plugins for SSR apps

Awesome Lists containing this project

README

          

# vite-ssr-components

**_vite-ssr-components_** provides JSX components and Vite plugins for helping the development and deployment for SSR application with Vite.

## Motivation

When using Cloudflare's Vite plugin for SSR applications, several challenges arise:

- **Missing Vite client scripts**: The Vite client script (`/@vite/client`) needs to be manually embedded in server-side rendered HTML
- **No SSR hot reload**: Server-side code changes don't trigger hot reloads during development
- **Complex asset path resolution**: Resolving script and asset paths after build requires manual `manifest.json` handling
- **Manual build configuration**: Manually specifying entry files in `vite.config.ts` for each Script/Link component

This library solves these issues by providing:

- **ViteClient component** for development mode
- **SSR hot reload** plugin for seamless development experience
- **Automatic asset path resolution** using Vite's `manifest.json`
- **Automatic build entry detection** from Script/Link components with flexible component-attribute mapping
- **Framework agnostic** components for both hono/jsx and React

## Install

```bash
npm i -D vite-ssr-components
```

## Quick Start

### 1. Add the SSR Plugin

```ts
// vite.config.ts
import { defineConfig } from 'vite'
import { cloudflare } from '@cloudflare/vite-plugin'
import ssrPlugin from 'vite-ssr-components/plugin'

export default defineConfig({
plugins: [cloudflare(), ssrPlugin()],
})
```

### 2. Use Components in Your SSR Code

```tsx
import { Script, Link, ViteClient } from 'vite-ssr-components/hono'
// import { Script, Link, ViteClient } from 'vite-ssr-components/react'

function App() {
return (










)
}
```

That's it! The plugin automatically:

- Scans your source files for Script/Link components
- Detects the referenced files from component attributes
- Adds the detected files to Vite's build input
- Configures client build settings
- Enables SSR hot reload

## Components

> [!IMPORTANT]
> If you define custom components with the same names as the default `Link` and `Script` components, unexpected behavior may occur. In such cases, use different names for your custom components or specify custom component names in the plugin's `components` configuration.

### ViteClient

Adds Vite client script for development mode.

```tsx
import { ViteClient } from 'vite-ssr-components/hono'
// import { ViteClient } from 'vite-ssr-components/react'

function App() {
return (





)
}
// Renders:
```

### Script

Adds script tags with proper Vite handling. In production mode, automatically reads `manifest.json` to resolve the correct built file paths.

```tsx
import { Script } from 'vite-ssr-components/hono'
// import { Script } from 'vite-ssr-components/react'

function App() {
return (





)
}
// Development:
// Production:
```

#### Options

- `src` (required): Source path of the script file
- `manifest`: Custom Vite manifest object (auto-loaded if not provided)
- `prod`: Force production mode (defaults to `import.meta.env.PROD`)
- `baseUrl`: Base URL for assets (defaults to `/`)
- All standard HTML script attributes are supported

```tsx
console.log('loaded')} />
```

### Link

Adds link tags for stylesheets and other resources. In production mode, automatically reads `manifest.json` to resolve the correct built file paths.

```tsx
import { Link } from 'vite-ssr-components/hono'
// import { Link } from 'vite-ssr-components/react'

function App() {
return (
<html>
<head>
<Link href='/src/style.css' rel='stylesheet' />
</head>
</html>
)
}
// Development: <link href="/src/style.css" rel="stylesheet">
// Production: <link href="/assets/style-def456.css" rel="stylesheet">
```

#### Options

- `href` (required): Source path of the resource
- `manifest`: Custom Vite manifest object (auto-loaded if not provided)
- `prod`: Force production mode (defaults to `import.meta.env.PROD`)
- `baseUrl`: Base URL for assets (defaults to `/`)
- All standard HTML link attributes are supported

```tsx
<Link href='/src/style.css' rel='stylesheet' baseUrl='/app/' media='screen' />
```

### ReactRefresh (React only)

Enables React Fast Refresh in development. Requires [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react) to be installed and configured.

```tsx
import { ReactRefresh } from 'vite-ssr-components/react'

function App() {
return (
<html>
<head>
<ReactRefresh />
</head>
</html>
)
}
// Adds React refresh runtime in development mode
```

## Plugins

### SSR Plugin

The main plugin that combines auto-entry detection and SSR hot reload functionality.

```ts
// vite.config.ts
import { defineConfig } from 'vite'
import ssrPlugin from 'vite-ssr-components/plugin'

export default defineConfig({
plugins: [ssrPlugin()],
})
```

#### Options

```ts
interface Component {
name: string // Component name to detect
attribute: string // Attribute name to extract file path from
}

interface SSRPluginOptions {
entry?: {
target?: string | string[] // File patterns to scan (default: '**/*.{tsx,ts}')
components?: Component[] // Component configurations (default: [{ name: 'Script', attribute: 'src' }, { name: 'Link', attribute: 'href' }])
}
hotReload?:
| boolean
| {
target?: string | string[] // File patterns to watch (default: ['**/*.ts', '**/*.tsx'])
ignore?: string | string[] // File patterns to ignore
}
}
```

When `target` / `entry` is omitted, both `autoEntry` and `ssrHotReload` scan
the whole project. The following directories are excluded automatically:

- **Fixed**: `node_modules`, `dist`, `build`, `out`, `coverage`, plus any
dotfile directories
- **Dynamic**: `build.outDir` and every `environments[*].build.outDir`
configured on the resolved Vite config

For `autoEntry`, files matching the pattern are also fast-skipped before
the AST parser runs when none of the configured component names appears as
a substring of the file — this keeps the wider default cheap on monorepos
and large codebases.

#### Examples

```ts
// Basic usage
export default defineConfig({
plugins: [ssrPlugin()],
})

// Custom file patterns and component configurations
export default defineConfig({
plugins: [
ssrPlugin({
entry: {
target: ['src/**/*.tsx', 'app/**/*.ts'], // Scan multiple patterns
components: [
{ name: 'Script', attribute: 'src' },
{ name: 'Link', attribute: 'href' },
{ name: 'CustomScript', attribute: 'source' },
{ name: 'CustomLink', attribute: 'url' },
{ name: 'Foo', attribute: 'bar' },
{ name: 'DataLoader', attribute: 'dataPath' },
{ name: 'AssetLoader', attribute: 'assetUrl' },
],
},
hotReload: {
target: ['src/**/*.ts', 'src/**/*.tsx'],
ignore: ['src/client/**/*'],
},
}),
],
})

// Scan only specific directories
export default defineConfig({
plugins: [
ssrPlugin({
entry: {
target: 'app/**/*.tsx', // Only scan app directory
},
}),
],
})

// Disable hot reload
export default defineConfig({
plugins: [
ssrPlugin({
hotReload: false,
}),
],
})
```

#### Auto-Entry Detection

The plugin automatically scans your source files to detect Script/Link components and extracts file paths from their attributes. This eliminates the need to manually configure build entries.

**How it works:**

1. **File Scanning**: The plugin scans files matching the specified patterns (default: `**/*.{tsx,ts}`, excluding `node_modules`, `dist`, `build`, `out`, `coverage`, dotfile dirs, and any configured `build.outDir`)
2. **Substring Prefilter**: Files that don't even mention `vite-ssr-components` are skipped before the AST parser runs
3. **AST Analysis**: Remaining files are parsed and analyzed to find JSX components
4. **Import-aware Detection**: Only `<Script>` / `<Link>` (or your custom components) imported from `vite-ssr-components` are picked up. Same-named components from other packages (e.g. `@inertiajs/react`'s `<Link>`, `next/script`) are ignored.
5. **Attribute Extraction**: File paths are extracted from the specified attributes
6. **Build Configuration**: Detected files are automatically added to Vite's build input

**Resolving the package**

When `vite-ssr-components` is installed normally, the plugin also accepts the
following two import shapes as belonging to the package:

- Relative imports that resolve to a file inside the on-disk package directory
(useful in monorepos that reach into the package via `../../`)
- Bare specifiers under another name (e.g. `@my/ssr`) whose resolved
`package.json` directory matches `vite-ssr-components` — covers workspace
links / aliases that point at the same package on disk

**Not supported**

The following import shapes are **not** detected, because resolving them
requires Vite's full resolver pipeline (which isn't available at the point
where auto-entry runs):

- `tsconfig.json` `paths` aliases (e.g. `@/components/Script`)
- Vite `resolve.alias` entries

If you rely on either, import the components via the canonical
`vite-ssr-components/<entry>` specifier in the files you want auto-entry to
scan.

> Detected `src` / `href` values are normalized to project-relative paths, so leading-slash forms like `<Script src="/src/client.tsx" />` are accepted by both rollup and Vite 8 / rolldown.

**Example:**

```tsx
// src/pages/home.tsx
function HomePage() {
return (
<html>
<head>
<Script src='/src/client.tsx' />
<Link href='/src/style.css' rel='stylesheet' />
</head>
</html>
)
}

// src/pages/about.tsx
function AboutPage() {
return (
<html>
<head>
<Script src='/src/about-client.tsx' />
<Link href='/src/about.css' rel='stylesheet' />
</head>
</html>
)
}
```

The plugin will automatically detect and add these files to the build:

- `/src/client.tsx`
- `/src/style.css`
- `/src/about-client.tsx`
- `/src/about.css`

#### Custom Component Detection

The plugin can detect any custom components with any attribute names:

```tsx
// Custom components in your SSR code
function App() {
return (
<html>
<head>
<CustomScript source='/src/app.js' />
<CustomLink url='/src/main.css' />
<Foo bar='/src/data.json' />
<DataLoader dataPath='/src/config.json' />
<AssetLoader assetUrl='/src/assets/image.png' />
</head>
</html>
)
}
```

The plugin will automatically:

- Detect `CustomScript` components and extract paths from the `source` attribute
- Detect `CustomLink` components and extract paths from the `url` attribute
- Detect `Foo` components and extract paths from the `bar` attribute
- And so on...

Each component type only looks for its specified attribute, ensuring accurate detection:

```tsx
// Only the correct attributes are detected for each component
<Script href="/src/wrong.css" src="/src/client.tsx" /> // Only 'src' is detected
<Link src="/src/wrong.js" href="/src/style.css" /> // Only 'href' is detected
```

#### File Pattern Matching

The plugin supports flexible file pattern matching using glob patterns:

```ts
// Single pattern
pattern: 'src/**/*.tsx'

// Multiple patterns
pattern: ['src/**/*.tsx', 'app/**/*.ts', 'components/**/*.jsx']

// Exclude specific directories
pattern: ['src/**/*.{tsx,ts}', '!src/test/**/*']
```

**Pattern Examples:**

- `src/**/*.tsx` - All .tsx files in src directory and subdirectories
- `app/**/*.{ts,tsx}` - All .ts and .tsx files in app directory
- `**/*.jsx` - All .jsx files in the entire project
- `!node_modules/**/*` - Exclude node_modules directory

## Examples

See the [examples](./examples) directory for complete working examples.

### hono/jsx

#### Directory Structure

```
examples/hono/
├── src/
│ ├── index.tsx # Server entry point
│ ├── client.tsx # Client entry point
│ └── style.css # Stylesheet
├── vite.config.ts # Vite configuration
└── package.json
```

#### vite.config.ts

```ts
import { defineConfig } from 'vite'
import { cloudflare } from '@cloudflare/vite-plugin'
import ssrPlugin from 'vite-ssr-components/plugin'

export default defineConfig({
plugins: [cloudflare(), ssrPlugin()],
})
```

#### Server Code

```tsx
import { Hono } from 'hono'
import { Script, Link, ViteClient } from 'vite-ssr-components/hono'

const app = new Hono()

app.get('/', (c) => {
return c.html(
<html>
<head>
<ViteClient />
<Script src='/src/client.tsx' />
<Link href='/src/style.css' rel='stylesheet' />
</head>
<body>
<div id='root' />
</body>
</html>
)
})
```

### React

#### Directory Structure

```
examples/react/
├── src/
│ ├── index.tsx # Server entry point
│ ├── style.css # Stylesheet
│ └── client/
│ ├── index.tsx # Client entry point
│ └── app.tsx # React app component
├── vite.config.ts # Vite configuration
└── package.json
```

#### vite.config.ts

```ts
import { defineConfig } from 'vite'
import { cloudflare } from '@cloudflare/vite-plugin'
import react from '@vitejs/plugin-react'
import ssrPlugin from 'vite-ssr-components/plugin'

export default defineConfig({
plugins: [
cloudflare(),
ssrPlugin({
hotReload: {
ignore: ['./src/client/**/*.tsx'],
},
}),
react(),
],
})
```

#### Server Code

```tsx
import { Hono } from 'hono'
import { Script, Link, ViteClient, ReactRefresh } from 'vite-ssr-components/react'
import { renderToReadableStream } from 'react-dom/server'

const app = new Hono()

app.get('/', async (c) => {
c.header('Content-Type', 'text/html')
return c.body(
await renderToReadableStream(
<html>
<head>
<ViteClient />
<ReactRefresh />
<Script src='/src/client/index.tsx' />
<Link href='/src/style.css' rel='stylesheet' />
</head>
<body>
<div id='root' />
</body>
</html>
)
)
})
```

## Author

Yusuke Wada <https://github.com/yusukebe>

## License

MIT