vite-ssr-components provides JSX components and Vite plugins for helping the development and deployment for SSR application with Vite.
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
npm i -D vite-ssr-components
// 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()],
})
import { Script, Link, ViteClient } from 'vite-ssr-components/hono'
// import { Script, Link, ViteClient } from 'vite-ssr-components/react'
function App() {
return (
<html>
<head>
<ViteClient />
<Script src='/src/client.tsx' />
<Link href='/src/style.css' rel='stylesheet' />
</head>
<body>
<div id='root' />
</body>
</html>
)
}
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
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.
Adds Vite client script for development mode.
import { ViteClient } from 'vite-ssr-components/hono'
// import { ViteClient } from 'vite-ssr-components/react'
function App() {
return (
<html>
<head>
<ViteClient />
</head>
</html>
)
}
// Renders: <script type="module" src="/@vite/client"></script>
Adds script tags with proper Vite handling. In production mode, automatically reads manifest.json
to resolve the correct built file paths.
import { Script } from 'vite-ssr-components/hono'
// import { Script } from 'vite-ssr-components/react'
function App() {
return (
<html>
<head>
<Script src='/src/client.tsx' />
</head>
</html>
)
}
// Development: <script type="module" src="/src/client.tsx"></script>
// Production: <script type="module" src="/assets/client-abc123.js"></script>
src
(required): Source path of the script filemanifest
: Custom Vite manifest object (auto-loaded if not provided)prod
: Force production mode (defaults toimport.meta.env.PROD
)baseUrl
: Base URL for assets (defaults to/
)- All standard HTML script attributes are supported
<Script src='/src/client.tsx' baseUrl='/app/' defer onLoad={() => console.log('loaded')} />
Adds link tags for stylesheets and other resources. In production mode, automatically reads manifest.json
to resolve the correct built file paths.
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">
href
(required): Source path of the resourcemanifest
: Custom Vite manifest object (auto-loaded if not provided)prod
: Force production mode (defaults toimport.meta.env.PROD
)baseUrl
: Base URL for assets (defaults to/
)- All standard HTML link attributes are supported
<Link href='/src/style.css' rel='stylesheet' baseUrl='/app/' media='screen' />
Enables React Fast Refresh in development. Requires @vitejs/plugin-react to be installed and configured.
import { ReactRefresh } from 'vite-ssr-components/react'
function App() {
return (
<html>
<head>
<ReactRefresh />
</head>
</html>
)
}
// Adds React refresh runtime in development mode
The main plugin that combines auto-entry detection and SSR hot reload functionality.
// vite.config.ts
import { defineConfig } from 'vite'
import ssrPlugin from 'vite-ssr-components/plugin'
export default defineConfig({
plugins: [ssrPlugin()],
})
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: 'src/**/*.{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: ['src/**/*.ts', 'src/**/*.tsx'])
ignore?: string | string[] // File patterns to ignore
}
}
// 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,
}),
],
})
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:
- File Scanning: The plugin scans files matching the specified patterns (default:
src/**/*.{tsx,ts}
) - AST Analysis: Each file is parsed and analyzed to find JSX components
- Component Detection: Components matching the configured names are detected
- Attribute Extraction: File paths are extracted from the specified attributes
- Build Configuration: Detected files are automatically added to Vite's build input
Example:
// 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
The plugin can detect any custom components with any attribute names:
// 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 thesource
attribute - Detect
CustomLink
components and extract paths from theurl
attribute - Detect
Foo
components and extract paths from thebar
attribute - And so on...
Each component type only looks for its specified attribute, ensuring accurate detection:
// 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
The plugin supports flexible file pattern matching using glob patterns:
// 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 subdirectoriesapp/**/*.{ts,tsx}
- All .ts and .tsx files in app directory**/*.jsx
- All .jsx files in the entire project!node_modules/**/*
- Exclude node_modules directory
See the examples directory for complete working examples.
examples/hono/
├── src/
│ ├── index.tsx # Server entry point
│ ├── client.tsx # Client entry point
│ └── style.css # Stylesheet
├── vite.config.ts # Vite configuration
└── package.json
import { defineConfig } from 'vite'
import { cloudflare } from '@cloudflare/vite-plugin'
import ssrPlugin from 'vite-ssr-components/plugin'
export default defineConfig({
plugins: [cloudflare(), ssrPlugin()],
})
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>
)
})
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
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(),
],
})
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>
)
)
})
Yusuke Wada https://github.com/yusukebe
MIT