added Material UI support
This commit is contained in:
+4
-1
@@ -16,7 +16,10 @@
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
"project": "./tsconfig.json",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"rules": {
|
||||
|
||||
+22
-10
@@ -21,22 +21,34 @@
|
||||
"autofill"
|
||||
],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.18.1",
|
||||
"devDependencies": {
|
||||
"@types/firefox-webext-browser": "^120.0.4",
|
||||
"@types/node": "^20.14.9",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.16.0",
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"@types/firefox-webext-browser": "^143.0.0",
|
||||
"@types/node": "^24.8.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.1",
|
||||
"@typescript-eslint/parser": "^8.46.1",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"jsdom": "^27.0.1",
|
||||
"prettier": "^3.3.2",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.2",
|
||||
"vite": "^7.1.10",
|
||||
"vite-plugin-web-extension": "^4.1.6",
|
||||
"vitest": "^1.6.0"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^7.3.4",
|
||||
"@mui/material": "^7.3.4",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
+1669
-811
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
+2
-52
@@ -4,59 +4,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>JobSpeed</title>
|
||||
<link rel="stylesheet" href="popup.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="popup-container">
|
||||
<header>
|
||||
<h1>JobSpeed</h1>
|
||||
<p class="subtitle">Auto-fill job applications</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="resume-section">
|
||||
<h2>Resume</h2>
|
||||
<div id="resume-status" class="status-container">
|
||||
<p class="status-text">No resume uploaded</p>
|
||||
</div>
|
||||
|
||||
<div class="upload-container">
|
||||
<input
|
||||
type="file"
|
||||
id="resume-file-input"
|
||||
accept=".pdf,.txt,.doc,.docx"
|
||||
style="display: none"
|
||||
/>
|
||||
<button id="upload-button" class="primary-button">
|
||||
Upload Resume
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="resume-preview" class="resume-preview hidden">
|
||||
<h3>Resume Data</h3>
|
||||
<div id="resume-data"></div>
|
||||
<button id="clear-resume-button" class="secondary-button">
|
||||
Clear Resume
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="actions-section">
|
||||
<h2>Actions</h2>
|
||||
<button id="scan-page-button" class="primary-button">
|
||||
Scan Current Page
|
||||
</button>
|
||||
<p class="help-text">
|
||||
Click to detect form fields on the current page and preview
|
||||
auto-fill suggestions
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p class="version">v1.0.0</p>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="popup.ts" type="module"></script>
|
||||
<div id="root"></div>
|
||||
<script src="popup.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
import type { ResumeData, MessageResponse } from './types';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const uploadButton = document.getElementById(
|
||||
'upload-button'
|
||||
) as HTMLButtonElement;
|
||||
const resumeFileInput = document.getElementById(
|
||||
'resume-file-input'
|
||||
) as HTMLInputElement;
|
||||
const resumeStatus = document.getElementById('resume-status');
|
||||
const resumePreview = document.getElementById('resume-preview');
|
||||
const resumeDataDiv = document.getElementById('resume-data');
|
||||
const clearResumeButton = document.getElementById(
|
||||
'clear-resume-button'
|
||||
) as HTMLButtonElement;
|
||||
const scanPageButton = document.getElementById(
|
||||
'scan-page-button'
|
||||
) as HTMLButtonElement;
|
||||
|
||||
// Load and display existing resume data
|
||||
void loadResumeStatus();
|
||||
|
||||
uploadButton.addEventListener('click', () => {
|
||||
resumeFileInput.click();
|
||||
});
|
||||
|
||||
resumeFileInput.addEventListener('change', (event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const text = await file.text();
|
||||
const resumeData = parseResume(text);
|
||||
|
||||
await browser.runtime.sendMessage({
|
||||
type: 'SAVE_RESUME_DATA',
|
||||
data: resumeData,
|
||||
});
|
||||
|
||||
await loadResumeStatus();
|
||||
alert('Resume uploaded successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error uploading resume:', error);
|
||||
alert('Error uploading resume. Please try again.');
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
clearResumeButton.addEventListener('click', () => {
|
||||
if (!confirm('Are you sure you want to clear your resume data?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
type: 'SAVE_RESUME_DATA',
|
||||
data: null,
|
||||
});
|
||||
|
||||
await loadResumeStatus();
|
||||
alert('Resume cleared successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error clearing resume:', error);
|
||||
alert('Error clearing resume. Please try again.');
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
scanPageButton.addEventListener('click', () => {
|
||||
void (async () => {
|
||||
try {
|
||||
const tabs = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
const activeTab = tabs[0];
|
||||
|
||||
if (!activeTab?.id) {
|
||||
alert('No active tab found');
|
||||
return;
|
||||
}
|
||||
|
||||
await browser.tabs.sendMessage(activeTab.id, {
|
||||
type: 'SCAN_PAGE',
|
||||
});
|
||||
|
||||
window.close();
|
||||
} catch (error) {
|
||||
console.error('Error scanning page:', error);
|
||||
alert('Error scanning page. Please try again.');
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
async function loadResumeStatus() {
|
||||
try {
|
||||
const response = (await browser.runtime.sendMessage({
|
||||
type: 'GET_RESUME_DATA',
|
||||
})) as MessageResponse;
|
||||
|
||||
if (response.success && response.data) {
|
||||
const resumeData = response.data as ResumeData;
|
||||
displayResumeData(resumeData);
|
||||
resumeStatus?.classList.add('has-resume');
|
||||
if (resumeStatus) {
|
||||
const statusText = resumeStatus.querySelector('.status-text');
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Resume uploaded ✓';
|
||||
}
|
||||
}
|
||||
resumePreview?.classList.remove('hidden');
|
||||
if (scanPageButton) {
|
||||
scanPageButton.disabled = false;
|
||||
}
|
||||
} else {
|
||||
resumeStatus?.classList.remove('has-resume');
|
||||
if (resumeStatus) {
|
||||
const statusText = resumeStatus.querySelector('.status-text');
|
||||
if (statusText) {
|
||||
statusText.textContent = 'No resume uploaded';
|
||||
}
|
||||
}
|
||||
resumePreview?.classList.add('hidden');
|
||||
if (scanPageButton) {
|
||||
scanPageButton.disabled = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading resume status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displayResumeData(data: ResumeData) {
|
||||
if (!resumeDataDiv) return;
|
||||
|
||||
const html = `
|
||||
<p><strong>Name:</strong> ${data.personalInfo.fullName}</p>
|
||||
<p><strong>Email:</strong> ${data.personalInfo.email}</p>
|
||||
<p><strong>Phone:</strong> ${data.personalInfo.phone}</p>
|
||||
<p><strong>Location:</strong> ${data.personalInfo.city}, ${data.personalInfo.state}</p>
|
||||
<p><strong>Skills:</strong> ${data.skills.slice(0, 5).join(', ')}${data.skills.length > 5 ? '...' : ''}</p>
|
||||
`;
|
||||
|
||||
resumeDataDiv.innerHTML = html;
|
||||
}
|
||||
|
||||
function parseResume(text: string): ResumeData {
|
||||
// Basic resume parser - extracts email, phone, and text content
|
||||
const emailRegex = /[\w.-]+@[\w.-]+\.\w+/g;
|
||||
const phoneRegex =
|
||||
/(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g;
|
||||
|
||||
const email = text.match(emailRegex)?.[0] || '';
|
||||
const phone = text.match(phoneRegex)?.[0] || '';
|
||||
|
||||
// Extract name (assume first line is name)
|
||||
const lines = text.split('\n').filter((line) => line.trim());
|
||||
const fullName = lines[0] || '';
|
||||
|
||||
// Basic skill extraction (look for common keywords)
|
||||
const skillKeywords = [
|
||||
'JavaScript',
|
||||
'TypeScript',
|
||||
'Python',
|
||||
'Java',
|
||||
'React',
|
||||
'Node.js',
|
||||
'SQL',
|
||||
'HTML',
|
||||
'CSS',
|
||||
'Git',
|
||||
'AWS',
|
||||
'Azure',
|
||||
'Docker',
|
||||
'Kubernetes',
|
||||
];
|
||||
const skills = skillKeywords.filter((skill) =>
|
||||
text.toLowerCase().includes(skill.toLowerCase())
|
||||
);
|
||||
|
||||
return {
|
||||
personalInfo: {
|
||||
fullName,
|
||||
email,
|
||||
phone,
|
||||
address: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zipCode: '',
|
||||
country: '',
|
||||
},
|
||||
workExperience: [],
|
||||
education: [],
|
||||
skills,
|
||||
summary: text.substring(0, 500),
|
||||
};
|
||||
}
|
||||
});
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {
|
||||
ThemeProvider,
|
||||
createTheme,
|
||||
CssBaseline,
|
||||
Box,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Typography,
|
||||
Paper,
|
||||
Button,
|
||||
Alert,
|
||||
Chip,
|
||||
Stack,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Upload as UploadIcon,
|
||||
Delete as DeleteIcon,
|
||||
Search as SearchIcon,
|
||||
Description as DescriptionIcon,
|
||||
} from '@mui/icons-material';
|
||||
import type { ResumeData } from './types';
|
||||
|
||||
// Create a custom theme for the extension
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#667eea',
|
||||
},
|
||||
secondary: {
|
||||
main: '#764ba2',
|
||||
},
|
||||
background: {
|
||||
default: '#f5f5f5',
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif',
|
||||
},
|
||||
});
|
||||
|
||||
const PopupApp: React.FC = () => {
|
||||
const [resumeData, setResumeData] = useState<ResumeData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void loadResumeStatus();
|
||||
}, []);
|
||||
|
||||
const loadResumeStatus = async () => {
|
||||
try {
|
||||
const response = (await browser.runtime.sendMessage({
|
||||
type: 'GET_RESUME_DATA',
|
||||
})) as { success: boolean; data?: ResumeData | null };
|
||||
|
||||
if (response.success && response.data) {
|
||||
setResumeData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading resume:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadResume = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.pdf,.txt,.doc,.docx';
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const parsedData = parseResume(text);
|
||||
|
||||
await browser.runtime.sendMessage({
|
||||
type: 'SAVE_RESUME_DATA',
|
||||
data: parsedData,
|
||||
});
|
||||
|
||||
await loadResumeStatus();
|
||||
alert('Resume uploaded successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error uploading resume:', error);
|
||||
alert('Error uploading resume. Please try again.');
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const handleClearResume = async () => {
|
||||
if (!confirm('Are you sure you want to clear your resume data?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
type: 'SAVE_RESUME_DATA',
|
||||
data: null,
|
||||
});
|
||||
|
||||
await loadResumeStatus();
|
||||
alert('Resume cleared successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error clearing resume:', error);
|
||||
alert('Error clearing resume. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleScanPage = async () => {
|
||||
try {
|
||||
const tabs = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
const activeTab = tabs[0];
|
||||
|
||||
if (!activeTab?.id) {
|
||||
alert('No active tab found');
|
||||
return;
|
||||
}
|
||||
|
||||
await browser.tabs.sendMessage(activeTab.id, {
|
||||
type: 'SCAN_PAGE',
|
||||
});
|
||||
|
||||
window.close();
|
||||
} catch (error) {
|
||||
console.error('Error scanning page:', error);
|
||||
alert('Error scanning page. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const parseResume = (text: string): ResumeData => {
|
||||
const lines = text.split('\n').filter((line) => line.trim());
|
||||
|
||||
return {
|
||||
personalInfo: {
|
||||
fullName:
|
||||
lines
|
||||
.find((line) => line.match(/^[A-Z][a-z]+ [A-Z][a-z]+/))
|
||||
?.trim() || '',
|
||||
email:
|
||||
lines
|
||||
.find((line) =>
|
||||
line.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/)
|
||||
)
|
||||
?.trim() || '',
|
||||
phone:
|
||||
lines
|
||||
.find((line) =>
|
||||
line.match(/(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/)
|
||||
)
|
||||
?.trim() || '',
|
||||
address:
|
||||
lines
|
||||
.find((line) => line.match(/\d+\s+\w+\s+(St|Ave|Rd|Blvd|Ln|Dr)/i))
|
||||
?.trim() || '',
|
||||
city: '',
|
||||
state: '',
|
||||
zipCode: '',
|
||||
country: '',
|
||||
},
|
||||
workExperience: [],
|
||||
education: [],
|
||||
skills: [],
|
||||
summary: '',
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Box sx={{ width: 400, bgcolor: 'background.default' }}>
|
||||
{/* Header */}
|
||||
<AppBar
|
||||
position="static"
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<Box sx={{ flexGrow: 1, textAlign: 'center' }}>
|
||||
<Typography variant="h5" component="h1" sx={{ fontWeight: 600 }}>
|
||||
JobSpeed
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ opacity: 0.9 }}>
|
||||
Auto-fill job applications
|
||||
</Typography>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{/* Main Content */}
|
||||
<Box sx={{ p: 2.5 }}>
|
||||
{/* Resume Section */}
|
||||
<Paper elevation={0} sx={{ p: 2, mb: 3, bgcolor: 'white' }}>
|
||||
<Stack spacing={2}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
Resume
|
||||
</Typography>
|
||||
{resumeData && (
|
||||
<Chip
|
||||
icon={<DescriptionIcon />}
|
||||
label="Uploaded"
|
||||
color="success"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{!resumeData ? (
|
||||
<Alert severity="info" sx={{ mb: 1 }}>
|
||||
No resume uploaded
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert severity="success" sx={{ mb: 1 }}>
|
||||
Resume data loaded
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<UploadIcon />}
|
||||
onClick={handleUploadResume}
|
||||
fullWidth
|
||||
sx={{
|
||||
background:
|
||||
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
textTransform: 'none',
|
||||
py: 1.5,
|
||||
}}
|
||||
>
|
||||
Upload Resume
|
||||
</Button>
|
||||
|
||||
{resumeData && (
|
||||
<>
|
||||
<Divider />
|
||||
<Box sx={{ bgcolor: '#f8f9fa', p: 2, borderRadius: 1 }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ mb: 1, fontWeight: 600 }}
|
||||
>
|
||||
Resume Data
|
||||
</Typography>
|
||||
<Stack
|
||||
spacing={0.5}
|
||||
sx={{ fontSize: '0.875rem', color: '#666' }}
|
||||
>
|
||||
{resumeData.personalInfo.fullName && (
|
||||
<Typography variant="body2">
|
||||
<strong>Name:</strong>{' '}
|
||||
{resumeData.personalInfo.fullName}
|
||||
</Typography>
|
||||
)}
|
||||
{resumeData.personalInfo.email && (
|
||||
<Typography variant="body2">
|
||||
<strong>Email:</strong>{' '}
|
||||
{resumeData.personalInfo.email}
|
||||
</Typography>
|
||||
)}
|
||||
{resumeData.personalInfo.phone && (
|
||||
<Typography variant="body2">
|
||||
<strong>Phone:</strong>{' '}
|
||||
{resumeData.personalInfo.phone}
|
||||
</Typography>
|
||||
)}
|
||||
{resumeData.personalInfo.address && (
|
||||
<Typography variant="body2">
|
||||
<strong>Address:</strong>{' '}
|
||||
{resumeData.personalInfo.address}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={() => void handleClearResume()}
|
||||
fullWidth
|
||||
color="error"
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
Clear Resume
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Actions Section */}
|
||||
<Paper elevation={0} sx={{ p: 2, bgcolor: 'white' }}>
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
Actions
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<SearchIcon />}
|
||||
onClick={() => void handleScanPage()}
|
||||
fullWidth
|
||||
sx={{
|
||||
background:
|
||||
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
textTransform: 'none',
|
||||
py: 1.5,
|
||||
}}
|
||||
>
|
||||
Scan Current Page
|
||||
</Button>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Click to detect form fields on the current page and preview
|
||||
auto-fill suggestions
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{/* Footer */}
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5,
|
||||
textAlign: 'center',
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
bgcolor: '#f8f9fa',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
v1.0.0
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// Mount the React app
|
||||
const container = document.getElementById('root');
|
||||
if (container) {
|
||||
const root = createRoot(container);
|
||||
root.render(<PopupApp />);
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import webExtension from 'vite-plugin-web-extension';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
@@ -8,6 +9,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
export default defineConfig({
|
||||
root: 'src',
|
||||
plugins: [
|
||||
react(),
|
||||
webExtension({
|
||||
manifest: 'manifest.json',
|
||||
watchFilePaths: ['**/*'],
|
||||
|
||||
Reference in New Issue
Block a user