offly
v0.1.8
Published
A lightweight plugin that makes any web app offline-first automatically
Maintainers
Readme
🚀 Offly
A lightweight plugin that makes any web app offline-first automatically.
Offly is a developer tool that transforms any web application (React, Vue, Next.js, Astro, Svelte, or Vanilla JS) into an offline-first app automatically — without developers writing or managing service workers, caching logic, or sync systems manually.
✨ Features
- 🗂️ Automatic Asset Caching - Detects and caches static assets (.js, .css, .png, etc.)
- 💾 Offline Data Persistence - Wraps fetch() calls with IndexedDB storage
- 🔄 Background Sync - Queues failed requests and replays them when online
- ⚡ Simple React/Vue Hooks - Use
useOfflineData()for effortless offline data - 🧰 CLI Tools -
Offly initandOffly buildcommands - 🌐 Service Worker Generation - Pre-configured service worker with caching strategies
- 📦 Zero Configuration - Works out of the box with sensible defaults
🚀 Quick Start
Installation
npm install OfflyInitialize Offly
npx Offly initThis command:
- Scans your project for assets and API calls
- Generates
Offly-sw.js(service worker) - Creates
.Offlyrc.json(configuration) - Creates
Offly-register.js(registration script)
Include in your HTML
<!DOCTYPE html>
<html>
<head>
<title>My Offline App</title>
</head>
<body>
<div id="root"></div>
<!-- Add this script -->
<script src="/Offly-register.js"></script>
</body>
</html>Build for production
npx Offly buildThis automatically injects service worker registration into your built HTML files.
📖 Usage
Basic Fetch Wrapper
Replace your fetch calls with OfflyFetch for automatic offline support:
import { OfflyFetch } from 'Offly';
// Network-first strategy (default for API calls)
const response = await OfflyFetch('/api/todos', {
Offly: {
strategy: 'network-first',
maxAge: 300 // 5 minutes
}
});
// Cache-first strategy (great for assets)
const response = await OfflyFetch('/api/user/profile', {
Offly: {
strategy: 'cache-first',
fallbackData: { name: 'Unknown User' }
}
});React Hooks
import { useOfflineData, useOfflinePost } from 'Offly/react';
function TodoList() {
// Automatically handles offline/online states
const { data: todos, loading, error, isOffline } = useOfflineData('/api/todos', {
fallbackData: [],
strategy: 'network-first'
});
const { post: addTodo, loading: adding } = useOfflinePost('/api/todos');
const handleAddTodo = async (title: string) => {
try {
const result = await addTodo({ title, completed: false });
if (result.queued) {
alert('Todo will be saved when you\'re back online!');
}
} catch (error) {
console.error('Failed to add todo:', error);
}
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{isOffline && <div className="offline-banner">🔴 Offline Mode</div>}
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
}Advanced Configuration
Create a custom configuration:
import { createOfflyConfig } from 'Offly';
const config = createOfflyConfig({
assets: {
patterns: ['**/*.js', '**/*.css', '**/*.woff2'],
strategy: 'cache-first',
maxAge: 7 * 24 * 60 * 60 // 7 days
},
api: {
baseUrl: '/api',
strategy: 'network-first',
maxAge: 10 * 60 // 10 minutes
},
sync: {
enabled: true,
maxRetries: 5,
retryDelay: 2000
},
debug: true
});🧰 CLI Commands
Offly init
Initializes Offly in your project.
npx Offly init [options]Options:
-d, --dir <directory>- Target directory (default: current directory)--skip-install- Skip automatic dependency installation
What it does:
- Scans your project for static assets and API endpoints
- Generates
Offly-sw.jsservice worker - Creates
.Offlyrc.jsonconfiguration file - Creates
Offly-register.jsregistration script
Offly build
Builds your project with offline support.
npx Offly build [options]Options:
-d, --dir <directory>- Build directory (default: dist)-c, --config <file>- Config file path (default: .Offlyrc.json)
What it does:
- Copies service worker to build directory
- Injects service worker registration into HTML files
- Generates cache manifest with asset list
⚙️ Configuration
Offly uses .Offlyrc.json for configuration:
{
"version": "0.1.0",
"assets": {
"patterns": ["**/*.js", "**/*.css", "**/*.png"],
"strategy": "cache-first",
"maxAge": 2592000
},
"api": {
"baseUrl": "/api",
"endpoints": [
{
"path": "/api/todos",
"method": "GET",
"cache": true,
"syncOnFailure": false
}
],
"strategy": "network-first",
"maxAge": 300
},
"sync": {
"enabled": true,
"maxRetries": 3,
"retryDelay": 1000
},
"debug": false
}Configuration Options
| Option | Type | Description |
|--------|------|-------------|
| assets.patterns | string[] | Glob patterns for assets to cache |
| assets.strategy | 'cache-first' \| 'network-first' | Caching strategy for assets |
| assets.maxAge | number | Cache duration in seconds |
| api.baseUrl | string | Base URL for API endpoints |
| api.strategy | 'cache-first' \| 'network-first' | Caching strategy for API calls |
| api.maxAge | number | Cache duration for API responses |
| sync.enabled | boolean | Enable background sync |
| sync.maxRetries | number | Max retry attempts for failed requests |
| sync.retryDelay | number | Delay between retries (ms) |
🎯 Caching Strategies
Cache-First
- Check cache first
- Return cached data if available and not expired
- Fetch from network if cache miss
- Update cache with fresh data
Best for: Static assets, user profiles, settings
Network-First
- Try network request first
- Return network data and update cache
- Fallback to cache if network fails
- Return cached data if available
Best for: Dynamic data, API responses, real-time content
Stale-While-Revalidate
- Return cached data immediately if available
- Fetch fresh data in background
- Update cache with fresh data for next request
Best for: Content that changes occasionally but needs fast loading
🔄 Background Sync
Offly automatically queues failed POST/PUT/PATCH/DELETE requests and replays them when the network returns:
import { useSyncStatus } from 'Offly/react';
function SyncStatus() {
const { total, pending, failed, syncing } = useSyncStatus();
return (
<div>
{syncing && <span>🔄 Syncing...</span>}
{pending > 0 && <span>📤 {pending} pending</span>}
{failed > 0 && <span>❌ {failed} failed</span>}
</div>
);
}🛠️ API Reference
Core Functions
OfflyFetch(url, options)
Enhanced fetch with offline support.
OfflyFetch(url: string | URL | Request, options?: RequestInit & {
Offly?: {
strategy?: 'cache-first' | 'network-first';
maxAge?: number;
fallbackData?: any;
}
}): Promise<Response>isOnline()
Check network status.
isOnline(): booleanonNetworkStatusChange(callback)
Listen for network status changes.
onNetworkStatusChange(callback: (isOnline: boolean) => void): () => voidReact Hooks
useOfflineData(url, options)
Fetch data with offline support.
useOfflineData<T>(url: string, options?: {
fallbackData?: T;
maxAge?: number;
strategy?: 'cache-first' | 'network-first';
}): {
data: T | null;
loading: boolean;
error: Error | null;
isOffline: boolean;
refetch: () => Promise<void>;
}useOfflinePost(url)
Post data with offline queueing.
useOfflinePost<T>(url: string): {
post: (data: T) => Promise<any>;
loading: boolean;
error: Error | null;
isOffline: boolean;
}Cache Management
CacheManager
Direct cache manipulation.
const cacheManager = new CacheManager();
await cacheManager.init();
// Store data
await cacheManager.set('/api/todos', 'GET', todos, 300);
// Retrieve data
const cached = await cacheManager.get('/api/todos', 'GET');
// Delete data
await cacheManager.deleteEntry('/api/todos', 'GET');
// Get statistics
const stats = await cacheManager.getStats();🎨 Framework Integration
React
// Install React types
npm install --save-dev @types/react
// Use hooks
import { useOfflineData } from 'Offly/react';Vue 3 (Composition API)
// Create a composable
import { ref, onMounted } from 'vue';
import { OfflyFetch, onNetworkStatusChange } from 'Offly';
export function useOfflineData(url: string) {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
const isOffline = ref(!navigator.onLine);
const fetchData = async () => {
loading.value = true;
try {
const response = await OfflyFetch(url);
data.value = await response.json();
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchData();
const cleanup = onNetworkStatusChange((online) => {
isOffline.value = !online;
if (online) fetchData();
});
return cleanup;
});
return { data, loading, error, isOffline, refetch: fetchData };
}Next.js
// pages/_app.tsx
import { useEffect } from 'react';
function MyApp({ Component, pageProps }) {
useEffect(() => {
// Register service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/Offly-sw.js');
}
}, []);
return <Component {...pageProps} />;
}Vanilla JavaScript
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<script type="module">
import { OfflyFetch, isOnline } from './node_modules/Offly/dist/index.mjs';
// Use OfflyFetch instead of fetch
async function loadData() {
try {
const response = await OfflyFetch('/api/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Failed to load data:', error);
}
}
// Check online status
console.log('Online:', isOnline());
</script>
</body>
</html>🚀 Examples
Todo App with Offline Support
import React, { useState } from 'react';
import { useOfflineData, useOfflinePost, useSyncStatus } from 'Offly/react';
interface Todo {
id: number;
title: string;
completed: boolean;
}
export default function TodoApp() {
const [newTodo, setNewTodo] = useState('');
const { data: todos, loading, error, isOffline, refetch } = useOfflineData<Todo[]>('/api/todos', {
fallbackData: []
});
const { post: addTodo, loading: adding } = useOfflinePost<Omit<Todo, 'id'>>('/api/todos');
const syncStatus = useSyncStatus();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!newTodo.trim()) return;
try {
const result = await addTodo({
title: newTodo,
completed: false
});
if (result.queued) {
alert('Todo will be saved when back online!');
}
setNewTodo('');
refetch();
} catch (error) {
console.error('Failed to add todo:', error);
}
};
return (
<div className="todo-app">
<header>
<h1>📝 Offline Todo App</h1>
<div className="status">
{isOffline ? '🔴 Offline' : '🌐 Online'}
{syncStatus.pending > 0 && ` • ${syncStatus.pending} pending`}
</div>
</header>
<form onSubmit={handleSubmit}>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add a todo..."
disabled={adding}
/>
<button type="submit" disabled={adding}>
{adding ? 'Adding...' : 'Add'}
</button>
</form>
{loading && <p>Loading todos...</p>}
{error && <p>Error: {error.message}</p>}
<ul className="todo-list">
{todos?.map((todo) => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
{todo.title}
</li>
))}
</ul>
</div>
);
}🛠 Troubleshooting
Service Worker Not Registering
Make sure the service worker file is served from the same origin:
<!-- ✅ Correct -->
<script>
navigator.serviceWorker.register('/Offly-sw.js');
</script>
<!-- ❌ Wrong -->
<script>
navigator.serviceWorker.register('https://cdn.example.com/Offly-sw.js');
</script>Cache Not Working
- Check if service worker is active in DevTools → Application → Service Workers
- Verify cache entries in DevTools → Application → Cache Storage
- Enable debug mode in
.Offlyrc.json:
{
"debug": true
}TypeScript Errors
Install the necessary type packages:
# For React
npm install --save-dev @types/react
# For DOM types
npm install --save-dev @types/webBuild Issues
Ensure your build tool serves the service worker file:
// Vite
export default {
build: {
rollupOptions: {
input: {
main: 'index.html',
sw: 'Offly-sw.js'
}
}
}
}
// Webpack
module.exports = {
entry: {
main: './src/index.js',
sw: './Offly-sw.js'
}
}🤝 Contributing
We welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
📄 License
This project is licensed under the Apache-2.0 License - see the LICENSE file for details.
🙏 Acknowledgments
- Workbox for service worker utilities
- Dexie.js for IndexedDB wrapper
- Commander.js for CLI interface
📚 Learn More
Made with ❤️ by developers, for developers who want offline-first apps without the complexity.
