Skip to content

Capture page DOM and screenshot, save to OPFS, backup to S3 #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 0 additions & 72 deletions background.js

This file was deleted.

File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
6 changes: 6 additions & 0 deletions manifest.json → public/manifest.json
Original file line number Diff line number Diff line change
@@ -18,6 +18,12 @@
"optional_host_permissions": [
"*://*\/*"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"icons": {
"16": "16.png",
"32": "32.png",
76 changes: 75 additions & 1 deletion options.html → public/options.html
Original file line number Diff line number Diff line change
@@ -124,6 +124,9 @@
<li class="nav-item">
<a class="nav-link" id="import-tab" data-bs-toggle="tab" href="#import" role="tab">📤 Bulk Import URLs</a>
</li>
<li class="nav-item">
<a class="nav-link" id="s3-tab" data-bs-toggle="tab" href="#s3" role="tab">🗄️ S3 Configuration</a>
</li>
</ul>

<div class="tab-content">
@@ -469,6 +472,76 @@ <h5 class="mb-0">Import Browser Cookies to Archiving Profile</h5>
</div>
</div>
</div>

<!-- S3 Configuration Tab -->
<div class="tab-pane fade" id="s3" role="tabpanel">
<div class="row mt-4">
<div class="col-md-8">
<form id="s3Form" class="needs-validation" novalidate>
<div class="mb-3">
<label for="s3_endpoint" class="form-label"><b>🌐 S3 Endpoint URL *</b></label>
<input type="url" class="form-control" id="s3_endpoint"
placeholder="https://s3.amazonaws.com">
<div class="form-text">
The endpoint URL for your S3 service. For AWS S3, use the regional endpoint (e.g., <code>https://s3.us-west-2.amazonaws.com</code>).
For other S3-compatible services, use their specific endpoint.
</div>
</div>

<div class="mb-3">
<label for="s3_region" class="form-label"><b>🌍 S3 Region *</b></label>
<input type="text" class="form-control" id="s3_region"
placeholder="us-east-1">
<div class="form-text">
The region where your S3 bucket is located (e.g., <code>us-east-1</code>, <code>eu-west-1</code>).
</div>
</div>

<div class="mb-3">
<label for="s3_bucket" class="form-label"><b>🪣 S3 Bucket Name *</b></label>
<input type="text" class="form-control" id="s3_bucket"
placeholder="my-archive-bucket">
<div class="form-text">
The name of the S3 bucket where screenshots and archives will be stored.
</div>
</div>

<div class="mb-3">
<label for="s3_access_key_id" class="form-label"><b>🔑 Access Key ID *</b></label>
<input type="text" class="form-control" id="s3_access_key_id"
placeholder="AKIAIOSFODNN7EXAMPLE">
<div class="form-text">
Your S3 access key ID. It's recommended to use a key with limited permissions.
</div>
</div>

<div class="mb-3">
<label for="s3_secret_access_key" class="form-label"><b>🔒 Secret Access Key *</b></label>
<input type="password" class="form-control" id="s3_secret_access_key"
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">
<div class="form-text">
Your S3 secret access key. This will be stored securely in your browser.
</div>
</div>

<div class="d-flex gap-2">
<button type="button" id="saveS3Config" class="btn btn-primary">
Save Configuration
<span class="status-indicator" id="s3ConfigStatus"></span>
</button>
<button type="button" id="testS3Credentials" class="btn btn-outline-secondary">
Test S3 Connection
<span class="status-indicator" id="s3TestStatus"></span>
</button>
</div>
<div class="form-text mt-2">
<span id="s3ConfigStatusText" class="ms-2"></span>
<span id="s3TestStatusText" class="ms-2"></span>
</div>
</form>
</div>
</div>
</div>
</div>

<footer>
@@ -507,7 +580,8 @@ <h5 class="mb-0">Import Browser Cookies to Archiving Profile</h5>
<!-- <script type="module" src="entries-tab.js"></script>
<script type="module" src="import-tab.js"></script>
<script type="module" src="personas-tab.js"></script>
<script type="module" src="cookies-tab.js"></script> -->
<script type="module" src="cookies-tab.js"></script>
<script type="module" src="s3-tab.js"></script> -->
<script type="module" src="options.js"></script>
</body>
</html>
161 changes: 161 additions & 0 deletions src/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// background.js

import {
addToArchiveBox,
captureScreenshot,
captureDom,
uploadToS3,
readFileFromOPFS,
} from "./utils.js";

chrome.runtime.onInstalled.addListener(function () {
chrome.contextMenus.create({
id: 'save_to_archivebox_ctxmenu',
title: 'Save to ArchiveBox',
});
});


chrome.runtime.onMessage.addListener(async (message) => {
const options_url = chrome.runtime.getURL('options.html') + `?search=${message.id}`;
console.log('i ArchiveBox Collector showing options.html', options_url);
if (message.action === 'openOptionsPage') {
await chrome.tabs.create({ url: options_url });
}
});


// Listeners for user-submitted actions

async function saveEntry(tab) {
const entry = {
id: crypto.randomUUID(),
url: tab.url,
timestamp: new Date().toISOString(),
tags: [],
title: tab.title,
favicon: tab.favIconUrl
};

// Save the entry first
const { entries = [] } = await chrome.storage.local.get('entries');
entries.push(entry);
await chrome.storage.local.set({ entries });

// Inject scripts - CSS now handled in popup.js
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['popup.js']
});
}

chrome.action.onClicked.addListener((tab, data) => saveEntry(tab));

chrome.contextMenus.onClicked.addListener((info, tab) => saveEntry(tab));

// Listeners for messages from other workers and contexts

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'archivebox_add') {
addToArchiveBox(message.body, sendResponse, sendResponse);
}
return true;
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'capture_screenshot') {
(async ()=> {
try {
const result = await captureScreenshot(message.timestamp);
if (result.ok) {
sendResponse(result);
} else {
throw new Error(result.errorMessage);
}
} catch (error) {
console.log("Failed to capture screenshot: ", error);
sendResponse({ok: false, errorMessage: String(error)});
}
})();

return true;
}
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'capture_dom') {
(async ()=> {
try {
const result = await captureDom(message.timestamp)
if (result.ok) {
sendResponse(result);
} else {
throw new Error(result.errorMessage);
}
} catch (error) {
console.log("Failed to capture DOM: ", error);
sendResponse({ok: false, errorMessage: String(error)});
}
})();

return true;
}
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'save_to_s3') {
(async ()=> {
try {
const data = await readFileFromOPFS(message.path);

if (!data) {
throw new Error('Failed to read file from OPFS');
}

const fileName = message.path.split('/').filter(part=>part.length > 0).pop();

console.log('filename: ', fileName);
const s3Url = await uploadToS3(fileName, data, message.contentType);
sendResponse({ok: true, url: s3Url});
} catch (error) {
console.log('Failed to upload to S3: ', error);
sendResponse({ok: false, errorMessage: String(error)});
}
})();
return true;
}
})


chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'test_s3') {
(async () => {
// Upload test file
try {
const fileName = `.connection_test_${Date.now()}.txt`;
const randomContent = Math.random().toString(36).substring(2, 15);
const testData = new TextEncoder().encode(randomContent);

const s3Url = await uploadToS3(fileName, testData, 'text/plain');

// Verify test file matches
const response = await fetch(s3Url);

if (response.ok) {
const responseText = await response.text();
const testPassed = responseText === randomContent;
sendResponse(testPassed ? 'success' : 'failure');
} else {
console.error(`Failed to fetch test content: ${response.status} ${response,statusText}`);
sendResponse('failure');
}
} catch (error) {
console.error('S3 credential test failed:', error);
sendResponse('failure');
}
})();
return true;
}
});


File renamed without changes.
12 changes: 12 additions & 0 deletions src/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'capture_dom') {
try {
const domContent = document.documentElement.outerHTML;
sendResponse({domContent: domContent})
} catch {
console.log("failed to download", chrome.runtime.lastError);
throw new Error(`failed to download: ${chrome.runtime.lastError}`);
}
}
});

File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 2 additions & 0 deletions options.js → src/options.js
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import { initializeImport } from './import-tab.js';
import { initializePersonasTab } from './personas-tab.js';
import { initializeCookiesTab } from './cookies-tab.js';
import { initializeConfigTab } from './config-tab.js';
import { initializeS3Tab } from './s3-tab.js';

// Initialize all tabs when options page loads
document.addEventListener('DOMContentLoaded', () => {
@@ -11,6 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
initializePersonasTab();
initializeCookiesTab();
initializeConfigTab();
initializeS3Tab();

function changeTab() {
if (window.location.hash && window.location.hash !== document.querySelector('a.nav-link.active').id) {
File renamed without changes.
92 changes: 85 additions & 7 deletions popup.js → src/popup.js
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
const IS_IN_POPUP = window.location.href.startsWith('chrome-extension://') && window.location.href.endsWith('/popup.html');
const IS_ON_WEBSITE = !window.location.href.startsWith('chrome-extension://');

window.popup_element = null; // Global reference to popup element
window.popup_element = null;
window.hide_timer = null;

window.closePopup = function () {
@@ -12,7 +12,7 @@ window.closePopup = function () {
console.log("close popup");
};

// handle escape key when popup doesn't have focus
// Handle escape key when popup doesn't have focus
document.addEventListener('keydown', (e) => {
if (e.key == 'Escape') {
closePopup();
@@ -65,6 +65,44 @@ async function sendToArchiveBox(url, tags) {
return { ok: ok, status: status};
}

async function sendCaptureMessage(messageType, timestamp) {
try {
const captureResponse = await new Promise((resolve, reject) => {
chrome.runtime.sendMessage({
type: messageType, timestamp: timestamp,
}, (response) => {
if (!response.ok) {
reject(`${response.errorMessage}`);
}
resolve(response);
});
})
return captureResponse;
} catch (error) {
return {ok: false, errorMessage: `Failed to capture: ${String(error)}`}
}
}

async function sendSaveToS3Message(path, contentType) {
try {
const s3Response = await new Promise((resolve, reject) => {
chrome.runtime.sendMessage({
type: 'save_to_s3',
path,
contentType,
}, (response) => {
if (!response.ok) {
reject(`${response.errorMessage}`);
}
resolve(response);
});
})
return s3Response;
} catch (error) {
return {ok: false, errorMessage: `Failed to save to S3: ${String(error)}`};
}
}

window.getCurrentEntry = async function() {
const { entries = [] } = await chrome.storage.local.get('entries');
let current_entry = entries.find(entry => entry.url === window.location.href);
@@ -121,6 +159,17 @@ window.updateSuggestions = async function() {
: '';
}

async function saveLocally(timestamp, messageType) {
// Save locally
const captureResponse = await sendCaptureMessage(messageType, timestamp);
if (captureResponse.ok) {
console.log(`i Saved ${captureResponse.fileName} to ${captureResponse.path}`);
return captureResponse.path;
} else {
console.log(`Error: Failed to save ${messageType} locally: ${captureResponse.errorMessage}` );
}
}

window.updateCurrentTags = async function() {
if (!popup_element) return;
const current_tags_div = popup_element.querySelector('.ARCHIVEBOX__current-tags');
@@ -152,7 +201,15 @@ window.updateCurrentTags = async function() {


window.createPopup = async function() {
const { current_entry } = await getCurrentEntry();
const { current_entry, entries } = await getCurrentEntry();

// Take the screenshot before the popup appears, but don't wait until it has
// been saved to S3
const screenshotPath = await saveLocally(current_entry.timestamp, 'capture_screenshot');
screenshotS3Promise = screenshotPath ? sendSaveToS3Message(screenshotPath, 'image/png') : undefined;

const domPath = await saveLocally(current_entry.timestamp, 'capture_dom');
const domS3Promise = domPath ? sendSaveToS3Message(domPath, 'text/html') : undefined;

// Create iframe container
document.querySelector('.archive-box-iframe')?.remove();
@@ -429,13 +486,11 @@ window.createPopup = async function() {
const input = popup.querySelector('input');
const suggestions_div = popup.querySelector('.ARCHIVEBOX__tag-suggestions');
const current_tags_div = popup.querySelector('.ARCHIVEBOX__current-tags');

// console.log('Getting current tags and suggestions');

// Initial display of current tags and suggestions
await window.updateCurrentTags();
await window.updateSuggestions();

// Add click handlers for suggestion badges
suggestions_div.addEventListener('click', async (e) => {
if (e.target.classList.contains('suggestion')) {
@@ -509,7 +564,7 @@ window.createPopup = async function() {

// Handle keyboard navigation

// handle escape key when popup has focus
// Handle escape key when popup has focus
input.addEventListener("keydown", async (e) => {
if (e.key === "Escape") {
e.stopPropagation();
@@ -657,6 +712,29 @@ window.createPopup = async function() {

// Initial resize
setTimeout(resizeIframe, 0);

// Update the current entry with the result of the S3 backups
if (screenshotS3Promise) {
const screenshotS3Response = await screenshotS3Promise;
if (screenshotS3Response.ok) {
console.log(`i Saved screenshot to S3 at ${screenshotS3Response.url}`);
current_entry.s3ScreenshotURL = screenshotS3Response.url;
} else {
console.log(`Failed to backup to S3: ${screenshotS3Response.errorMessage}`);
}
}

if (domS3Promise) {
const domS3Response = await domS3Promise;
if (domS3Response.ok) {
console.log(`i Saved DOM to S3 at ${domS3Response.url}`);
current_entry.s3domURL = domS3Response.url;
} else {
console.log(`Failed to backup to S3: ${domS3Response.errorMessage}`);
}
}

await chrome.storage.local.set({ entries });
}

window.createPopup();
81 changes: 81 additions & 0 deletions src/s3-tab.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// S3 Configuration tab initialization and handlers
import { updateStatusIndicator } from './utils.js';

export async function initializeS3Tab() {
const s3Form = document.getElementById('s3Form');
const endpoint = document.getElementById('s3_endpoint');
const region = document.getElementById('s3_region');
const bucket = document.getElementById('s3_bucket');
const accessKeyId = document.getElementById('s3_access_key_id');
const secretAccessKey = document.getElementById('s3_secret_access_key');

const { s3config } = await chrome.storage.sync.get(['s3config']);

if (s3config) {
endpoint.value = s3config.endpoint || '';
region.value = s3config.region || '';
bucket.value = s3config.bucket || '';
accessKeyId.value = s3config.accessKeyId || '';
secretAccessKey.value = s3config.secretAccessKey || '';
}

document.getElementById('saveS3Config').addEventListener('click', async () => {
const statusIndicator = document.getElementById('s3ConfigStatus');
const statusText = document.getElementById('s3ConfigStatusText');

let endpointValue = endpoint.value.trim();

if (endpointValue && !endpointValue.startsWith('http://') && !endpointValue.startsWith('https://')) {
endpointValue = 'https://' + endpointValue;
}

if (endpointValue.endsWith('/')) {
endpointValue = endpointValue.slice(0, -1);
}

try {
const config = {
endpoint: endpointValue,
region: region.value.trim(),
bucket: bucket.value.trim(),
accessKeyId: accessKeyId.value.trim(),
secretAccessKey: secretAccessKey.value.trim()
};

// Check for missing or empty values
for (const [key, value] of Object.entries(config)) {
if (value === undefined || value === null || value === '') {
throw new Error(`Missing or empty configuration field: ${key}`);
}
}
console.log('Saving config: ', config);
chrome.storage.sync.set({ s3config: config });
updateStatusIndicator(statusIndicator, statusText, true, '✓ S3 Configuration saved successfully');
} catch (err) {
updateStatusIndicator(statusIndicator, statusText, false, `✗ Failed to save configuration: ${err.message}`);
}
});

document.getElementById('testS3Credentials').addEventListener('click', async () => {
const statusIndicator = document.getElementById('s3TestStatus');
const statusText = document.getElementById('s3TestStatusText');

statusText.innerHTML = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Testing S3 credentials...
`;

console.log("Testing connection to S3...")
try {
const response = await chrome.runtime.sendMessage({ type: 'test_s3' });

if (response === 'success') {
updateStatusIndicator(statusIndicator, statusText, true, '✓ Connection to S3 successful');
} else {
updateStatusIndicator(statusIndicator, statusText, false, `✗ Connection to S3 failed`);
}
} catch (err) {
updateStatusIndicator(statusIndicator, statusText, false, `✗ Error testing credentials: ${err.message}`);
}
});
}
192 changes: 190 additions & 2 deletions utils.js → src/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Common utility functions

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

// Helper to get server URL with fallback to legacy config name
export async function getArchiveBoxServerUrl() {
const { archivebox_server_url } = await chrome.storage.local.get(['archivebox_server_url']); // new ArchiveBox Extension v2.1.3 location
@@ -52,7 +54,7 @@ export async function addToArchiveBox(addCommandArgs, onComplete, onError) {
}

if (archivebox_api_key) {
// try ArchiveBox v0.8.0+ API endpoint first
// Try ArchiveBox v0.8.0+ API endpoint first
try {
const response = await fetch(`${archivebox_server_url}/api/v1/cli/add`, {
headers: {
@@ -77,7 +79,7 @@ export async function addToArchiveBox(addCommandArgs, onComplete, onError) {
}
}

// fall back to pre-v0.8.0 endpoint for backwards compatibility
// Fall back to pre-v0.8.0 endpoint for backwards compatibility
console.log('i addToArchiveBox using legacy /add POST method');

const parsedAddCommandArgs = JSON.parse(addCommandArgs);
@@ -207,3 +209,189 @@ export async function syncToArchiveBox(entry) {
};
}
}

// Helper: Only process pages that are "real" (skip about:blank, chrome://newtab, etc.)
function isRealPage(url) {
return url !== "about:blank" && !url.startsWith("chrome://newtab");
}

export async function captureScreenshot(timestamp) {
const activeTabs = await chrome.tabs.query({active: true, currentWindow: true});
const tab = activeTabs[0];
if (
!tab.url ||
!isRealPage(tab.url) ||
tab.url.startsWith("chrome-extension://")
) {
return;
}

// Capture the visible tab as a PNG data URL.
const dataUrl = await new Promise((resolve, reject) => {
chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" }, (dataUrl) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(dataUrl);
}
});
});

try {
const base64 = dataUrl.split(",")[1];
const byteString = atob(base64);
const arrayBuffer = new ArrayBuffer(byteString.length);
const uint8Array = new Uint8Array(arrayBuffer);

for (let i = 0; i < byteString.length; i++) {
uint8Array[i] = byteString.charCodeAt(i);
}

const blob = new Blob([uint8Array], { type: "image/png" });

const root = await navigator.storage.getDirectory();

const screenshotsDir = await root.getDirectoryHandle('screenshots', { create: true });

const fileName = `screenshot-${timestamp}.png`;

const fileHandle = await screenshotsDir.getFileHandle(fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(blob);
await writable.close();

console.log(`Screenshot saved to OPFS: /screenshots/${fileName}`);
return { ok: true, fileName, path: `/screenshots/${fileName}` };
} catch (error) {
console.error("Failed to save screenshot to OPFS:", error);
return { ok: false, errorMessage: String(error) };
}
}


export async function captureDom(timestamp) {
try {
const activeTabs = await chrome.tabs.query({active: true, currentWindow: true})
const tabId = activeTabs[0].id;

// Send a message to the content script
const captureResponse = await chrome.tabs.sendMessage( tabId, { type: 'capture_dom' } );

const fileName = `${timestamp}.html`;

const blob = new Blob([captureResponse.domContent], { type: "text/html" });

try {
const root = await navigator.storage.getDirectory();

const domDir = await root.getDirectoryHandle('dom', { create: true });

const fileHandle = await domDir.getFileHandle(fileName, { create: true });

const writable = await fileHandle.createWritable();
await writable.write(blob);
await writable.close();

console.log(`DOM content saved to OPFS: /dom/${fileName}`);
return { ok: true, fileName, path: `/dom/${fileName}` };
} catch (error) {
console.error("Failed to save DOM to OPFS:", error);
throw error;
}
} catch (error) {
console.log("Failed to capture dom:", error);
return { ok: false }
}
}

export async function uploadToS3(fileName, data, contentType) {
// Get saved config and make client
const s3Config = await new Promise((resolve) => {
chrome.storage.sync.get(['s3config'], (result) => {
resolve(result.s3config || {});
});
});

console.log('S3 config: ', s3Config);

if (!s3Config.endpoint || !s3Config.bucket || !s3Config.accessKeyId || !s3Config.secretAccessKey) {
throw new Error('S3 configuration is incomplete');
}

const client = new S3Client({
endpoint: s3Config.endpoint,
credentials: {
accessKeyId: s3Config.accessKeyId,
secretAccessKey: s3Config.secretAccessKey,
},
region: s3Config.region,
forcePathStyle: true,
requestChecksumCalculation: "WHEN_REQUIRED",
})

// Use custom headers for our requests
client.middlewareStack.add(
(next) =>
async (args) => {
const request = args.request;

const headers = request.headers;
delete headers["x-amz-checksum-crc32"];
delete headers["x-amz-checksum-crc32c"];
delete headers["x-amz-checksum-sha1"];
delete headers["x-amz-checksum-sha256"];
request.headers = headers;

Object.entries(request.headers).forEach(
([key, value]) => {
if (!request.headers) {
request.headers = {};
}
(request.headers)[key] = value;
}
);

return next(args);
},
{ step: "build", name: "customHeaders" }
);

// Send to S3
const command = new PutObjectCommand({
Bucket: s3Config.bucket,
Key: fileName,
Body: data,
ContentType: contentType,
});

await client.send(command);
return `${s3Config.endpoint}/${s3Config.bucket}/${fileName}`;
}

export async function readFileFromOPFS(path) {
try {
const root = await navigator.storage.getDirectory();

const pathParts = path.split('/').filter(part => part.length > 0);
if (pathParts.length === 0) {
throw new Error('Invalid path: empty path');
}
const fileName = pathParts.pop();

let currentDir = root;
for (const dirName of pathParts) {
currentDir = await currentDir.getDirectoryHandle(dirName);
}
const fileHandle = await currentDir.getFileHandle(fileName);
const file = await fileHandle.getFile();

// Return either bytes or text
if (file.type.startsWith('image/') || file.type === 'application/octet-stream') {
return new Uint8Array(await file.arrayBuffer());
}
return await file.text();
} catch (error) {
console.error(`Error reading file from OPFS at path ${path}:`, error);
return null;
}
}
33 changes: 33 additions & 0 deletions vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
server: {
watch: {
ignored: ['!src/**', '!public/**'], // Ensure Vite watches both directories
}
},
build: {
sourcemap: true,
rollupOptions: {
input: {
background: resolve(__dirname, 'src/background.js'),
content: resolve(__dirname, 'src/content.js'),
popup: resolve(__dirname, 'src/popup.js'),
utils: resolve(__dirname, 'src/utils.js'),
options: resolve(__dirname, 'src/options.js'),
configTab: resolve(__dirname, 'src/config-tab.js'),
cookiesTab: resolve(__dirname, 'src/cookies-tab.js'),
entriesTab: resolve(__dirname, 'src/entries-tab.js'),
importTab: resolve(__dirname, 'src/import-tab.js'),
personasTab: resolve(__dirname, 'src/personas-tab.js'),
s3Tab: resolve(__dirname, 'src/s3-tab.js')
},
output: {
// All built JS files will be output at the root as [name].js
entryFileNames: '[name].js'
}
},
outDir: 'dist'
}
});