vite-plugin-react-strangler
v1.5.1
Published
A Vite plugin for gradually migrating legacy server-side frontends to React using the Strangler Fig pattern
Maintainers
Readme
vite-plugin-react-strangler
A Vite plugin for gradually migrating legacy server-side frontends to React using the Strangler Fig pattern.
The Problem
Legacy server-side applications (PHP, Rails, Django, etc.) struggle with frontend modernization because traditional approaches require:
- Complete rewrites instead of gradual migration
- Complex integration between server templates and React
- Manual component registration and loading
- All-or-nothing bundle loading that hurts performance
The Solution
This plugin enables incremental React adoption in existing server applications by:
- Automatic component discovery - just add React components to a folder
- Web component generation - use
<hello-widget>directly in server templates - Intelligent loading - components load only when actually used on the page
- Zero integration complexity - one script tag replaces all manual setup
Automatically discovers React components, generates web component bundles, and creates a manifest for dynamic loading in any server-side framework.
Installation
npm install --save-dev vite-plugin-react-strangler
npm install react react-domQuick Start
1. Configure Vite
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { reactStrangler } from 'vite-plugin-react-strangler';
export default defineConfig({
plugins: [
react(),
reactStrangler()
]
});2. Create React Components
// components/Hello.tsx
import React, { useState } from 'react';
import styles from './Hello.module.css';
interface HelloProps {
name?: string;
onInputChange?: (value: string) => void;
}
export default function Hello({ name = "World", onInputChange }: HelloProps) {
const [value, setValue] = useState(name);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVal = e.target.value;
setValue(newVal);
onInputChange?.(newVal);
};
return (
<div className={styles.hello}>
<h3>Hello from React, {value}!</h3>
<input value={value} onChange={handleChange} />
</div>
);
}By default the plugin looks for components in components/ or src/components/. Using a different structure? Pass components: 'app/widgets' (or an array) to reactStrangler() and everything keeps working.
3. Build
npm run buildThe plugin will automatically:
- 🔍 Discover all components by scanning detected directories (defaults to
components/orsrc/components/) - 📦 Build individual component files (Hello.js, Card.js, etc.)
- 🎨 Extract and bundle CSS with hashed names
- 🌀 Inline discovery-level global CSS (Tailwind, base styles, etc.) so they load with
discover.js - 📄 Generate manifest listing all available components with CSS references
- 🧩 Register as web components (hello-widget, card-widget, etc.)
- Shadow DOM encapsulation to isolate component styles from surrounding PHP markup
- Generate type definitions on the web components for TypeScript support
Don’t have a discover.js yet? The plugin generates a minimal version automatically so you can start embedding widgets immediately.
4. Use in Any Server Framework
PHP
<script type="module" src="dist/discover.js"></script>
<hello-widget name="<?= $user['name'] ?>"></hello-widget>Ruby on Rails
<%= javascript_include_tag "dist/discover.js", type: "module" %>
<hello-widget name="<%= @user.name %>"></hello-widget>Django
{% load static %}
<script type="module" src="{% static 'dist/discover.js' %}"></script>
<hello-widget name="{{ user.name }}"></hello-widget>Generated Output
The plugin automatically generates:
dist/
├── discover.js # Discovery runtime + inlined global styles
├── components.manifest.json # Component registry with CSS refs
├── Hello.js # Individual component bundles
├── Card.js
├── DataTable.js
└── assets/
├── Hello-*.css # Hashed CSS modules
├── Card-*.css
└── jsx-runtime-*.js # Shared React runtimeAutomatic Features
- 🔍 Component Discovery - Auto-detects component folders and scans for .tsx/.jsx files
- 📦 Individual Bundles - Each component becomes a separate .js file
- 🎨 CSS Extraction - CSS modules bundled with hashed names
- 🌐 Global CSS Support - Plain
.css/.scss/.sass/.lessfiles auto-load once per component - 🛡️ Shadow DOM Isolation - Every component renders inside a shadow root so styles never leak into legacy pages
- 📄 Manifest Generation - Components listed with paths and CSS references
- 🧩 Web Component Registration - Auto-registers as custom elements
- ✅ CSS Loading - Automatically loads component styles when used
PostCSS & Tailwind CSS
- Honors any
postcss.config.*(or inline plugin configuration) and re-processes extracted component CSS so tools like Tailwind, Autoprefixer, or cssnano run automatically. - Plain CSS imports (e.g.
import './styles.css';) are recognized as component-scoped globals; they flow through PostCSS and load exactly once when the component script runs. - When the discovery entry (
discover.js) emits a global CSS asset—such as a Tailwind base file—the plugin inlines the compiled stylesheet intodiscover.jsand rewrites the emitted file. Including<script type="module" src="dist/discover.js"></script>is enough to make those utilities available on every page.
To enable Tailwind you can follow the standard Vite setup and the plugin will do the rest:
npm install -D tailwindcss @tailwindcss/postcss
npx tailwindcss init --ts// tailwind.config.ts
import type { Config } from 'tailwindcss';
export default {
content: ['./components/**/*.{js,jsx,ts,tsx}', './public/**/*.{php,html}'],
theme: { extend: {} },
plugins: [],
} satisfies Config;// postcss.config.cjs
module.exports = {
plugins: {
'@tailwindcss/postcss': {}
}
};/* components/styles/tailwind.css */
@import "tailwindcss";
@layer components {
.strangler-section-title {
@apply text-xl font-semibold;
}
}With that in place, importing the stylesheet once (e.g. import './components/styles/tailwind.css'; inside discover.js) is all that’s required—the plugin detects the discovery CSS output, inlines it into discover.js, and serves the utilities on every page. Component-level Tailwind usage works out of the box, and you can mix in module or global SCSS/Less files as shown in the PHP example.
If the tailwindcss CLI is present in your project, the plugin will also run it per component (using the component source as the content scope) and emit slim Tailwind bundles like assets/Card-tailwind.css. These are automatically referenced in the manifest so consumers can opt-in to component-scoped Tailwind. When the CLI is missing, the plugin simply falls back to the shared discovery stylesheet.
Shadow DOM Rendering
Every registered widget now renders into its own Shadow DOM root. The plugin injects the component’s CSS (module output, plain CSS/SCSS/Less, Tailwind extractions, etc.) directly into that shadow root, ensuring:
- Legacy PHP markup keeps its original styling (no Tailwind preflight bleed).
- Component styles can’t leak out, and external styles can’t accidentally override React widgets.
- Styling remains deterministic regardless of where the custom element is placed.
Configuration Options
interface ReactStranglerOptions {
/** Directories to scan for React components (auto-detected by default) */
components?: string | string[];
/** Discovery system entry point override (auto-detected or generated) */
discoveryEntry?: string;
/** PostCSS integration configuration */
postcss?: boolean | ReactStranglerPostCSSOptions;
}Control output locations and manifest destinations through standard Vite settings (e.g. build.outDir).
- Components: Automatically picks
components/orsrc/components/when present, falling back tosrc/orcomponents/if nothing matches. Supplycomponents(string or array) to override. - Discovery entry: Uses the first
discover.{js,ts,jsx,tsx}it finds in the project root orsrc/. If none exist, a stub is generated in.vite-strangler-temp/that exports the discovery runtime so you can ship immediately. - Manifest copies: The manifest now lives alongside your build output. If you need to duplicate it, add a post-build script tailored to your deployment.
Component Naming Convention
The plugin automatically generates web component names:
Hello.tsx→hello-widgetUserCard.tsx→usercard-widgetDataTable.tsx→datatable-widgetCard/Card.jsx→card-widget
How It Works
- Build Time: Plugin auto-discovers components from detected directories
- Bundle Generation: Creates individual .js files for each component
- CSS Extraction: Bundles CSS modules with hashed names
- Manifest Creation: Lists all components with JS/CSS file references
- Runtime Loading: Discovery system loads components and CSS on-demand
Browser API
The discovery system provides these global functions:
// List available components
window.componentDiscovery.listAvailable()
// Get loading stats
window.componentDiscovery.getStats()
// Load specific component
await window.componentDiscovery.loadComponent('hello-widget')
// Load multiple components synchronously (prevents FOUC)
await window.loadCriticalComponents(['hello-widget', 'card-widget'])Project Structure
your-ui-library/
├── components/
│ ├── Hello.tsx
│ ├── Hello.module.css
│ ├── Card/
│ │ ├── Card.jsx
│ │ └── Card.module.css
│ └── DataTable.tsx
├── vite.config.js
├── discover.js # Entry point for discovery system
└── dist/ # Generated by plugin
├── discover.js
├── components.manifest.json # Component registry with CSS refs
├── Hello.js # Individual component bundles
├── Card.js
└── assets/ # CSS and shared assetsExamples
See examples/php-example for a complete working PHP application demonstrating component discovery, critical vs async loading, and Docker setup.
License
MIT
