mirror of
https://github.com/zadam/trilium.git
synced 2025-12-05 23:14:24 +01:00
Copilot Integration: - Streamlined instructions (70% shorter than original) - Task templates for common operations - Three-tier usage strategy (free/strategic/manual) - Optimized for GitHub Copilot Basic tier limits Development Resources: - Common task workflows with time estimates - Feature parity checklist with priorities - Debugging and troubleshooting guides - Testing scenarios and checklists - Code quality standards Workflow Optimization: - Efficient Copilot task budgeting - Real-world implementation examples - Performance and success metrics - Project completion roadmap Reduces repetitive context in prompts. Maximizes limited Copilot task budget.
14 KiB
14 KiB
MV2 to MV3 Migration Patterns
Quick reference for common migration scenarios when implementing features from the legacy extension.
Pattern 1: Background Page → Service Worker
MV2 (Don't Use)
// Persistent background page with global state
let cachedData = {};
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
cachedData[msg.id] = msg.data;
sendResponse({success: true});
});
MV3 (Use This)
// Stateless service worker with chrome.storage
import { Logger } from '@/shared/utils';
const logger = Logger.create('BackgroundHandler', 'background');
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
(async () => {
try {
// Store in chrome.storage, not memory
await chrome.storage.local.set({ [msg.id]: msg.data });
logger.info('Data stored', { id: msg.id });
sendResponse({ success: true });
} catch (error) {
logger.error('Storage failed', error);
sendResponse({ success: false, error: error.message });
}
})();
return true; // Required for async sendResponse
});
Key Changes:
- No global state (service worker can terminate)
- Use
chrome.storagefor persistence - Always return
truefor async handlers - Centralized logging for debugging
Pattern 2: Content Script DOM Manipulation
MV2 Pattern
// Simple DOM access
const content = document.body.innerHTML;
MV3 Pattern (Same, but with error handling)
import { Logger } from '@/shared/utils';
const logger = Logger.create('ContentExtractor', 'content');
function extractContent(): string {
try {
if (!document.body) {
logger.warn('Document body not available');
return '';
}
const content = document.body.innerHTML;
logger.debug('Content extracted', { length: content.length });
return content;
} catch (error) {
logger.error('Content extraction failed', error);
return '';
}
}
Key Changes:
- Add null checks for DOM elements
- Use centralized logging
- Handle errors gracefully
Pattern 3: Screenshot Capture
MV2 Pattern
chrome.tabs.captureVisibleTab(null, {format: 'png'}, (dataUrl) => {
// Crop using canvas
const canvas = document.createElement('canvas');
// ... cropping logic
});
MV3 Pattern
import { Logger } from '@/shared/utils';
const logger = Logger.create('ScreenshotCapture', 'background');
async function captureAndCrop(
tabId: number,
cropRect: { x: number; y: number; width: number; height: number }
): Promise<string> {
try {
// Step 1: Capture full tab
const dataUrl = await chrome.tabs.captureVisibleTab(null, {
format: 'png'
});
logger.info('Screenshot captured', { tabId });
// Step 2: Crop using OffscreenCanvas (MV3 service worker compatible)
const response = await fetch(dataUrl);
const blob = await response.blob();
const bitmap = await createImageBitmap(blob);
const offscreen = new OffscreenCanvas(cropRect.width, cropRect.height);
const ctx = offscreen.getContext('2d');
if (!ctx) {
throw new Error('Could not get canvas context');
}
ctx.drawImage(
bitmap,
cropRect.x, cropRect.y, cropRect.width, cropRect.height,
0, 0, cropRect.width, cropRect.height
);
const croppedBlob = await offscreen.convertToBlob({ type: 'image/png' });
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(croppedBlob);
});
} catch (error) {
logger.error('Screenshot crop failed', error);
throw error;
}
}
Key Changes:
- Use
OffscreenCanvas(available in service workers) - No DOM canvas manipulation in background
- Full async/await pattern
- Comprehensive error handling
Pattern 4: Image Processing
MV2 Pattern
// Download image and convert to base64
function processImage(imgSrc) {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', imgSrc);
xhr.responseType = 'blob';
xhr.onload = () => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(xhr.response);
};
xhr.send();
});
}
MV3 Pattern
import { Logger } from '@/shared/utils';
const logger = Logger.create('ImageProcessor', 'background');
async function downloadAndEncodeImage(
imgSrc: string,
baseUrl: string
): Promise<string> {
try {
// Resolve relative URLs
const absoluteUrl = new URL(imgSrc, baseUrl).href;
logger.debug('Downloading image', { url: absoluteUrl });
// Use fetch API (modern, async)
const response = await fetch(absoluteUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const blob = await response.blob();
// Convert to base64
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = () => reject(new Error('FileReader failed'));
reader.readAsDataURL(blob);
});
} catch (error) {
logger.warn('Image download failed', { url: imgSrc, error });
// Return original URL as fallback
return imgSrc;
}
}
Key Changes:
- Use
fetch()instead ofXMLHttpRequest - Handle CORS errors gracefully
- Return original URL on failure (don't break the note)
- Resolve relative URLs properly
Pattern 5: Context Menu Creation
MV2 Pattern
chrome.contextMenus.create({
id: "save-selection",
title: "Save to Trilium",
contexts: ["selection"]
});
MV3 Pattern (Same API, better structure)
import { Logger } from '@/shared/utils';
const logger = Logger.create('ContextMenu', 'background');
interface MenuConfig {
id: string;
title: string;
contexts: chrome.contextMenus.ContextType[];
}
const MENU_ITEMS: MenuConfig[] = [
{ id: 'save-selection', title: 'Save Selection to Trilium', contexts: ['selection'] },
{ id: 'save-page', title: 'Save Page to Trilium', contexts: ['page'] },
{ id: 'save-link', title: 'Save Link to Trilium', contexts: ['link'] },
{ id: 'save-image', title: 'Save Image to Trilium', contexts: ['image'] },
{ id: 'save-screenshot', title: 'Save Screenshot to Trilium', contexts: ['page'] }
];
async function setupContextMenus(): Promise<void> {
try {
// Remove existing menus
await chrome.contextMenus.removeAll();
// Create all menu items
for (const item of MENU_ITEMS) {
await chrome.contextMenus.create(item);
logger.debug('Context menu created', { id: item.id });
}
logger.info('Context menus initialized', { count: MENU_ITEMS.length });
} catch (error) {
logger.error('Context menu setup failed', error);
}
}
// Call during service worker initialization
chrome.runtime.onInstalled.addListener(() => {
setupContextMenus();
});
Key Changes:
- Centralized menu configuration
- Clear typing with interfaces
- Proper error handling
- Logging for debugging
Pattern 6: Sending Messages from Content to Background
MV2 Pattern
chrome.runtime.sendMessage({type: 'SAVE', data: content}, (response) => {
console.log('Saved:', response);
});
MV3 Pattern
import { Logger } from '@/shared/utils';
const logger = Logger.create('ContentScript', 'content');
interface SaveMessage {
type: 'SAVE_SELECTION' | 'SAVE_PAGE' | 'SAVE_LINK';
data: {
content: string;
metadata: {
title: string;
url: string;
};
};
}
interface SaveResponse {
success: boolean;
noteId?: string;
error?: string;
}
async function sendToBackground(message: SaveMessage): Promise<SaveResponse> {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(message, (response: SaveResponse) => {
if (chrome.runtime.lastError) {
logger.error('Message send failed', chrome.runtime.lastError);
reject(new Error(chrome.runtime.lastError.message));
return;
}
if (!response.success) {
logger.warn('Background operation failed', { error: response.error });
reject(new Error(response.error));
return;
}
logger.info('Message handled successfully', { noteId: response.noteId });
resolve(response);
});
});
}
// Usage
try {
const result = await sendToBackground({
type: 'SAVE_SELECTION',
data: {
content: selectedHtml,
metadata: {
title: document.title,
url: window.location.href
}
}
});
showToast(`Saved to Trilium: ${result.noteId}`);
} catch (error) {
logger.error('Save failed', error);
showToast('Failed to save to Trilium', 'error');
}
Key Changes:
- Strong typing for messages and responses
- Promise wrapper for callback API
- Always check
chrome.runtime.lastError - Handle errors at both send and response levels
Pattern 7: Storage Operations
MV2 Pattern
// Mix of localStorage and chrome.storage
localStorage.setItem('setting', value);
chrome.storage.local.get(['data'], (result) => {
console.log(result.data);
});
MV3 Pattern
import { Logger } from '@/shared/utils';
const logger = Logger.create('StorageManager', 'background');
// NEVER use localStorage in service workers - it doesn't exist
interface StorageData {
settings: {
triliumUrl: string;
authToken: string;
saveFormat: 'html' | 'markdown' | 'both';
};
cache: {
lastSync: number;
noteIds: string[];
};
}
async function loadSettings(): Promise<StorageData['settings']> {
try {
const { settings } = await chrome.storage.local.get(['settings']);
logger.debug('Settings loaded', { hasToken: !!settings?.authToken });
return settings || getDefaultSettings();
} catch (error) {
logger.error('Settings load failed', error);
return getDefaultSettings();
}
}
async function saveSettings(settings: Partial<StorageData['settings']>): Promise<void> {
try {
const current = await loadSettings();
const updated = { ...current, ...settings };
await chrome.storage.local.set({ settings: updated });
logger.info('Settings saved', { keys: Object.keys(settings) });
} catch (error) {
logger.error('Settings save failed', error);
throw error;
}
}
function getDefaultSettings(): StorageData['settings'] {
return {
triliumUrl: '',
authToken: '',
saveFormat: 'html'
};
}
Key Changes:
- NEVER use
localStorage(not available in service workers) - Use
chrome.storage.localfor all data - Use
chrome.storage.syncfor user preferences (sync across devices) - Full TypeScript typing for stored data
- Default values for missing data
Pattern 8: Trilium API Communication
MV2 Pattern
function saveToTrilium(content, metadata) {
const xhr = new XMLHttpRequest();
xhr.open('POST', triliumUrl + '/api/notes');
xhr.setRequestHeader('Authorization', token);
xhr.send(JSON.stringify({content, metadata}));
}
MV3 Pattern
import { Logger } from '@/shared/utils';
const logger = Logger.create('TriliumAPI', 'background');
interface TriliumNote {
title: string;
content: string;
type: 'text';
mime: 'text/html' | 'text/markdown';
parentNoteId?: string;
}
interface TriliumResponse {
note: {
noteId: string;
title: string;
};
}
async function createNote(
note: TriliumNote,
triliumUrl: string,
authToken: string
): Promise<string> {
try {
const url = `${triliumUrl}/api/create-note`;
logger.debug('Creating note in Trilium', {
title: note.title,
contentLength: note.content.length
});
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': authToken,
'Content-Type': 'application/json'
},
body: JSON.stringify(note)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data: TriliumResponse = await response.json();
logger.info('Note created successfully', { noteId: data.note.noteId });
return data.note.noteId;
} catch (error) {
logger.error('Note creation failed', error);
throw error;
}
}
Key Changes:
- Use
fetch()API (modern, promise-based) - Full TypeScript typing for requests/responses
- Comprehensive error handling
- Detailed logging for debugging
Quick Reference: When to Use Each Pattern
| Task | Pattern | Files Typically Modified |
|---|---|---|
| Add capture feature | Pattern 1, 6, 8 | background/index.ts, content/index.ts |
| Process images | Pattern 4 | background/index.ts |
| Add context menu | Pattern 5 | background/index.ts |
| Screenshot with crop | Pattern 3 | background/index.ts, possibly content/screenshot.ts |
| Settings management | Pattern 7 | options/index.ts, background/index.ts |
| Trilium communication | Pattern 8 | background/index.ts |
Common Gotchas
-
Service Worker Termination
- Don't store state in global variables
- Use
chrome.storageorchrome.alarms
-
Async Message Handlers
- Always return
truein listener - Always check
chrome.runtime.lastError
- Always return
-
Canvas in Service Workers
- Use
OffscreenCanvas, not regular<canvas> - No DOM access in background scripts
- Use
-
CORS Issues
- Handle fetch failures gracefully
- Provide fallbacks for external resources
-
Type Safety
- Define interfaces for all messages
- Type all chrome.storage data structures
Usage: When implementing a feature, find the relevant pattern above and adapt it. Don't copy MV2 code directly—use these proven MV3 patterns instead.