Vite Integration
@resource-fallback/vite-plugin is a Vite 4+ plugin that provides runtime retry and multi-CDN fallback for Vite build outputs (sync JS/CSS and async chunks); for modulepreload failures it provides managed error suppression and rule gating, but does not replace the preload URL directly.
Installation
pnpm add -D @resource-fallback/vite-pluginBasic configuration
// vite.config.ts
import { defineConfig } from 'vite';
import resourceFallback from '@resource-fallback/vite-plugin';
export default defineConfig({
base: 'https://cdn.example.com/',
plugins: [
resourceFallback({
rules: [
{
base: 'https://cdn.example.com/',
urls: [
'https://cdn-backup.example.com/',
'/', // origin fallback
],
},
],
}),
],
});Important
Vite base should equal rules[].base (rule base) so build output URLs hit the rule.
Full options: Configuration Reference.
How it works
The plugin follows this build/runtime sequence:
1. configResolved: compare normalized Vite base against rule bases
The plugin first reads Vite's final resolved base in configResolved, then compares it with every rules[].base using the same trailing-slash normalization.
- If the normalized Vite
basematches at least one rulebase, URL rewriting stays enabled for later build steps - If it does not match,
writeBundleskips the dynamic import rewrite so a non-CDN build is not rewritten into external URLs
2. generateBundle: optionally emit Service Worker assets
If serviceWorker is enabled, generateBundle optionally emits:
rf-sw.jsmanifest.json
3. writeBundle: parse literal dynamic imports and replace them with window.__RF__.load(filename)
writeBundle uses es-module-lexer to parse dynamic imports inside chunks, then rewrites only literal imports that satisfy all of the following:
- the import specifier is a literal string
- the resolved filename exists in
chunk.dynamicImports - the build has already passed the normalized base gate
Matching imports are replaced with window.__RF__.load(filename), for example:
// Original code
const mod = await import('./Lazy.vue');
// After build
const mod = await window.__RF__.load('assets/Lazy-abc.js');Current window.__RF__.url(filename) semantics
window.__RF__.url(filename) only joins the filename with the first compiled rule base to build the initial URL. It does not inspect circuit state or skip hosts at page runtime.
Current window.__RF__.load(filename) recovery semantics
window.__RF__.load(filename) resolves the initial URL first, then delegates retry / fallback / deadline / cancellation to the shared RecoveryCoordinator. It also uses a normalized sharing key so concurrent loads from the same owner for the same logical resource can share one recovery Promise.
4. transformIndexHtml: inject runtime and preconnect tags
transformIndexHtml injects into <head>:
<link rel="preconnect">tags- a
<script>that inlines the runtime IIFE and theinstall(config)call
vite:preloadError handling
The runtime listens for Vite's vite:preloadError event. When modulepreload fails:
- the adapter reads
event.payload - if the extracted URL matches a configured rule, it calls
event.preventDefault() - it does not update circuit state, emit a recovery event, or choose a fallback URL directly
- CSS
<link>failures remain Observer-owned; this handler only stops Vite from throwing on a managed failure
Configuration example
resourceFallback({
rules: [
{
base: 'https://cdn.example.com/',
urls: ['https://cdn-backup.example.com/', 'https://static.mysite.com/', '/'],
retry: { max: 2, baseDelay: 300, maxDelay: 3000, jitter: true },
circuit: { threshold: 3, cooldown: 30000 },
},
],
debug: 'auto',
sri: 'strip',
nonce: 'my-csp-nonce',
injectPreconnect: true,
htmlInject: 'head-prepend',
});With @vitejs/plugin-legacy
When using @vitejs/plugin-legacy for SystemJS legacy bundles, the runtime installs the SystemJS adapter automatically via System.constructor.prototype.instantiate.
import legacy from '@vitejs/plugin-legacy';
import resourceFallback from '@resource-fallback/vite-plugin';
export default defineConfig({
base: 'https://cdn.example.com/',
plugins: [
legacy({ targets: ['defaults', 'not IE 11'] }),
resourceFallback({
rules: [{ base: 'https://cdn.example.com/', urls: ['/'] }],
}),
],
});Vite dev mode
The plugin is inactive in dev by default (enableDev: false). Vite dev uses native ESM — dynamic import failures cannot be intercepted.
Verification
Use vite build && vite preview to verify fallback. Setting enableDev: true injects the runtime in dev, but only sync <script> / <link> error events work.
Sync/async coverage
| Scenario | Vite (build/preview) | Vite (dev) |
|---|---|---|
Sync <script> / <link> | ✓ Observer | ✓ Observer |
Async chunk (import()) | ✓ __RF__.load + writeBundle rewrite | ✗ |
| CSS dynamic injection | ✓ Observer | ✓ Observer |
| SystemJS (legacy bundle) | ✓ instantiate hook | — |
| Images / fonts / media | ✓ Hybrid SW (opt-in) | ✗ |
CSS url() / @font-face | ✓ Hybrid SW (opt-in) | ✗ |
CSS @import | ✓ Hybrid SW (CSS referrer must match manifest) | ✗ |