Initial commit

This commit is contained in:
zs
2026-02-28 20:05:15 +08:00
commit c66f5f9be4
185 changed files with 18356 additions and 0 deletions

24
extension/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
extension/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
extension/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

13
extension/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>extension</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

33
extension/manifest.json Normal file
View File

@@ -0,0 +1,33 @@
{
"manifest_version": 3,
"name": "InsightReply",
"version": "1.0.0",
"description": "InsightReply 是一个帮助创始人在行业热点中增强社交表达并且输出高质评论的助手",
"action": {
"default_popup": "index.html"
},
"background": {
"service_worker": "src/background/index.ts",
"type": "module"
},
"permissions": [
"storage",
"activeTab"
],
"host_permissions": [
"https://twitter.com/*",
"https://x.com/*"
],
"content_scripts": [
{
"js": [
"src/content/index.ts",
"src/content/sidebar-mount.ts"
],
"matches": [
"https://twitter.com/*",
"https://x.com/*"
]
}
]
}

3139
extension/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
extension/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "extension",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"clsx": "^2.1.1",
"tailwind-merge": "^3.5.0",
"vue": "^3.5.25"
},
"devDependencies": {
"@crxjs/vite-plugin": "^2.0.0-beta.33",
"@tailwindcss/postcss": "^4.2.1",
"@types/chrome": "^0.1.37",
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.2",
"@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.27",
"postcss": "^8.5.6",
"tailwindcss": "^4.2.1",
"typescript": "~5.9.3",
"vite": "^7.3.1",
"vue-tsc": "^3.1.5"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

59
extension/src/App.vue Normal file
View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import { ref } from 'vue'
import { cn } from './lib/utils'
const isLoading = ref(false)
const triggerMockLoading = () => {
isLoading.value = true
setTimeout(() => {
isLoading.value = false
}, 1000)
}
</script>
<template>
<div class="w-[400px] h-[600px] bg-[#0A0A0A]/90 backdrop-blur-xl border border-white/10 text-[#E5E5E5] p-6 flex flex-col font-sans">
<!-- Title Area -->
<div class="mb-6">
<h1 class="text-xl font-medium tracking-tight bg-gradient-to-r from-violet-500 to-blue-500 bg-clip-text text-transparent inline-block">
InsightReply
</h1>
<p class="text-xs text-zinc-400 mt-1">Social Insight Copilot</p>
</div>
<!-- Main Content Area -->
<div class="flex-1 overflow-y-auto pr-2 space-y-4">
<!-- Example Heat Score Card -->
<div class="bg-[#171717] rounded-xl p-4 border border-white/5 shadow-lg shadow-black/50">
<div class="flex justify-between items-center mb-2">
<span class="text-sm font-medium text-zinc-300">Current Tweet Heat</span>
<span class="text-xs font-bold px-2 py-0.5 rounded-full bg-orange-500/20 text-orange-400 border border-orange-500/20">Hot</span>
</div>
<div class="flex items-end gap-2">
<span class="text-3xl font-semibold tracking-tighter">87.5</span>
<span class="text-xs text-zinc-500 mb-1">/ 100</span>
</div>
</div>
<!-- Action Button -->
<button
@click="triggerMockLoading"
:disabled="isLoading"
:class="cn(
'w-full py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ease-in-out',
'flex items-center justify-center gap-2',
isLoading
? 'bg-[#171717] text-zinc-500 border border-white/5 cursor-not-allowed'
: 'bg-brand-primary hover:bg-brand-primary/90 text-white shadow-lg shadow-brand-primary/20 hover:scale-[0.98]'
)"
>
<span v-if="isLoading" class="animate-spin inline-block w-4 h-4 border-2 border-white/20 border-t-white rounded-full"></span>
{{ isLoading ? 'Generating Insights...' : 'Generate Replies' }}
</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,16 @@
@import "tailwindcss";
@theme {
--color-brand-primary: #8B5CF6;
--color-brand-secondary: #3B82F6;
}
@layer base {
body {
@apply bg-[#0A0A0A] text-[#E5E5E5] antialiased;
}
::selection {
background-color: rgb(139 92 246 / 0.3);
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,39 @@
/**
* InsightReply Background Script
* 负责中转消息、管理 OAuth 以及与 Go 后端 API 通信
*/
console.log('InsightReply Background Script Loaded');
const API_BASE = 'http://localhost:8080/api/v1';
chrome.runtime.onMessage.addListener((message: { type: string; payload?: any }, _sender: chrome.runtime.MessageSender, sendResponse: (response?: any) => void) => {
if (message.type === 'SHOW_INSIGHT') {
console.log('Received tweet data in background:', message.payload);
}
if (message.type === 'GENERATE_REPLY') {
const { tweetContent, strategy, identity } = message.payload;
fetch(`${API_BASE}/ai/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tweet_content: tweetContent,
strategy: strategy,
identity: identity || 'Independent Developer / Founder'
})
})
.then(resp => resp.json())
.then(data => {
sendResponse({ success: true, data: data.data });
})
.catch(err => {
console.error('API Error:', err);
sendResponse({ success: false, error: err.message });
});
return true; // Keep channel open for async response
}
return true;
});

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@@ -0,0 +1,143 @@
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
tweetData?: {
author: string;
content: string;
stats: {
replies: string;
retweets: string;
likes: string;
}
}
}>()
const isVisible = ref(true)
const selectedStrategy = ref('Insightful')
const generatedReply = ref('')
const isGenerating = ref(false)
const strategies = [
{ id: 'Insightful', label: '认知升级型', icon: '🧠' },
{ id: 'Humorous', label: '幽默风趣型', icon: '😄' },
{ id: 'Professional', label: '专业严谨型', icon: '⚖️' },
{ id: 'Supportive', label: '共鸣支持型', icon: '❤️' },
{ id: 'Critical', label: '锐评批判型', icon: '🔥' }
]
const generate = () => {
if (!props.tweetData) return
isGenerating.value = true
chrome.runtime.sendMessage({
type: 'GENERATE_REPLY',
payload: {
tweetContent: props.tweetData.content,
strategy: selectedStrategy.value,
identity: 'Independent Developer / Founder' // Could be dynamic later
}
}, (response) => {
isGenerating.value = false
if (response && response.success) {
generatedReply.value = response.data.reply
} else {
generatedReply.value = 'Failed to generate reply. Please check your connection or API key.'
}
})
}
const copyToClipboard = () => {
navigator.clipboard.writeText(generatedReply.value)
}
</script>
<template>
<div v-if="isVisible" class="fixed right-4 top-20 w-[360px] max-h-[85vh] flex flex-col bg-[#0A0A0A]/90 backdrop-blur-2xl border border-white/10 rounded-2xl shadow-2xl text-[#E5E5E5] font-sans z-[9999] overflow-hidden">
<!-- Header -->
<div class="p-4 border-b border-white/5 flex justify-between items-center bg-white/5">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-brand-primary animate-pulse"></div>
<span class="text-sm font-medium tracking-wide">InsightReply AI</span>
</div>
<button @click="isVisible = false" class="text-zinc-500 hover:text-white transition-colors">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<!-- Content -->
<div class="p-5 flex-1 overflow-y-auto space-y-6">
<!-- Tweet Context (Small Preview) -->
<div v-if="tweetData" class="bg-white/5 rounded-xl p-3 border border-white/5">
<div class="text-[10px] text-zinc-500 mb-1 font-mono uppercase">Context</div>
<div class="text-xs text-zinc-400 line-clamp-2 italic">" {{ tweetData.content }} "</div>
</div>
<!-- Strategy Selector -->
<div class="space-y-3">
<span class="text-xs font-semibold text-zinc-500 uppercase tracking-widest">Generation Strategy</span>
<div class="grid grid-cols-1 gap-2">
<button
v-for="s in strategies"
:key="s.id"
@click="selectedStrategy = s.id"
:class="[
'flex items-center gap-3 p-3 rounded-xl border transition-all duration-200 text-sm group',
selectedStrategy === s.id
? 'bg-brand-primary/20 border-brand-primary text-white shadow-lg shadow-brand-primary/10'
: 'bg-white/5 border-transparent hover:bg-white/10 text-zinc-400'
]"
>
<span class="text-lg">{{ s.icon }}</span>
<span class="flex-1 text-left">{{ s.label }}</span>
<div v-if="selectedStrategy === s.id" class="w-1.5 h-1.5 rounded-full bg-brand-primary"></div>
</button>
</div>
</div>
<!-- Result Area -->
<div v-if="generatedReply" class="space-y-3 animate-in fade-in slide-in-from-bottom-2 duration-500">
<div class="flex justify-between items-center">
<span class="text-xs font-semibold text-zinc-500 uppercase tracking-widest">Assistant Suggestion</span>
<button @click="copyToClipboard" class="text-[10px] text-brand-primary hover:underline">Copy Result</button>
</div>
<div class="bg-[#171717] rounded-xl p-4 border border-white/10 text-sm leading-relaxed whitespace-pre-wrap">
{{ generatedReply }}
</div>
</div>
</div>
<!-- Footer Action -->
<div class="p-4 bg-white/5 border-t border-white/5">
<button
@click="generate"
:disabled="isGenerating"
class="w-full py-3 bg-gradient-to-r from-violet-600 to-blue-600 hover:from-violet-500 hover:to-blue-500 disabled:from-zinc-800 disabled:to-zinc-800 disabled:text-zinc-500 disabled:cursor-not-allowed text-white rounded-xl text-sm font-semibold transition-all shadow-xl shadow-brand-primary/20 flex items-center justify-center gap-2"
>
<div v-if="isGenerating" class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
{{ isGenerating ? 'AI Thinking...' : 'Generate High-Quality Reply' }}
</button>
</div>
</div>
</template>
<style scoped>
/* 局部样式保证不溢出 */
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
</style>

View File

@@ -0,0 +1,107 @@
/**
* InsightReply Content Script
* 负责解析 X (Twitter) 页面 DOM提取推文内容并注入交互按钮
*/
console.log('InsightReply Content Script Loaded');
// 1. 定义推文数据结构
interface TweetData {
id: string;
author: string;
content: string;
stats: {
replies: string;
retweets: string;
likes: string;
};
}
// 2. 提取推文内容的逻辑
const extractTweetData = (tweetElement: HTMLElement): TweetData | null => {
try {
// 根据 X 的 DOM 结构提取 (可能会随 Twitter 更新而变化)
const textElement = tweetElement.querySelector('[data-testid="tweetText"]');
const authorElement = tweetElement.querySelector('[data-testid="User-Name"]');
const linkElement = tweetElement.querySelector('time')?.parentElement as HTMLAnchorElement;
// 互动数据提取
const getStat = (testid: string) => {
const el = tweetElement.querySelector(`[data-testid="${testid}"]`);
return el?.getAttribute('aria-label') || '0';
};
if (!textElement || !authorElement || !linkElement) return null;
const tweetId = linkElement.href.split('/').pop() || '';
return {
id: tweetId,
author: authorElement.textContent || 'Unknown',
content: textElement.textContent || '',
stats: {
replies: getStat('reply'),
retweets: getStat('retweet'),
likes: getStat('like'),
}
};
} catch (e) {
console.error('Failed to extract tweet data:', e);
return null;
}
};
// 3. 注入“Insight”按钮
const injectInsightButton = (tweetElement: HTMLElement) => {
// 查找操作栏 (Actions bar)
const actionBar = tweetElement.querySelector('[role="group"]');
if (!actionBar || actionBar.querySelector('.insight-reply-btn')) return;
// 创建按钮
const btnContainer = document.createElement('div');
btnContainer.className = 'insight-reply-btn';
btnContainer.style.display = 'flex';
btnContainer.style.alignItems = 'center';
btnContainer.style.marginLeft = '12px';
btnContainer.style.cursor = 'pointer';
// 按钮内部图标 (简易版)
btnContainer.innerHTML = `
<div style="padding: 4px; border-radius: 9999px; transition: background 0.2s;" onmouseover="this.style.background='rgba(139, 92, 246, 0.1)'" onmouseout="this.style.background='transparent'">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" style="color: #8B5CF6;">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path>
</svg>
</div>
`;
btnContainer.onclick = (e) => {
e.stopPropagation();
const data = extractTweetData(tweetElement);
console.log('Target Tweet Data:', data);
// 发送消息给插件侧边栏/Popup (后续实现)
if (data) {
chrome.runtime.sendMessage({ type: 'SHOW_INSIGHT', payload: data });
}
};
actionBar.appendChild(btnContainer);
};
// 4. 定时或监听 DOM 变化进行扫描
const scanTweets = () => {
const tweets = document.querySelectorAll('article[data-testid="tweet"]');
tweets.forEach((tweet) => {
injectInsightButton(tweet as HTMLElement);
});
};
// 使用 MutationObserver 监听动态加载
const observer = new MutationObserver(() => {
scanTweets();
});
observer.observe(document.body, { childList: true, subtree: true });
// 初始扫描
scanTweets();

View File

@@ -0,0 +1,51 @@
import { createApp } from 'vue'
import Sidebar from './Sidebar.vue'
import '../assets/tailwind.css' // We might need to handle this specially for Shadow DOM
const MOUNT_ID = 'insight-reply-sidebar-root'
function initSidebar(tweetData?: any) {
if (document.getElementById(MOUNT_ID)) return
// 1. Create Host Element
const host = document.createElement('div')
host.id = MOUNT_ID
document.body.appendChild(host)
// 2. Create Shadow Root
const shadowRoot = host.attachShadow({ mode: 'open' })
// 3. Create Container for Vue
const container = document.createElement('div')
container.id = 'app'
shadowRoot.appendChild(container)
// 4. Inject Styles into Shadow DOM
// Note: In development/build, we need to find the generated CSS and inject it.
// CRXJS usually puts CSS in <link> tags in the head for content scripts.
// For Shadow DOM, we need to move or clone them into the shadow root.
const injectStyles = () => {
const styles = document.querySelectorAll('style, link[rel="stylesheet"]')
styles.forEach(style => {
// Only clone styles that look like they belong to our extension
// This is a heuristic, in a real build we'd use the asset URL
shadowRoot.appendChild(style.cloneNode(true))
})
}
// Initial injection
injectStyles()
// 5. Create Vue App
const app = createApp(Sidebar, { tweetData })
app.mount(container)
console.log('InsightReply Sidebar Mounted in Shadow DOM');
}
// Listen for messages to show/hide or update data
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'SHOW_INSIGHT') {
initSidebar(message.payload)
}
});

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

5
extension/src/main.ts Normal file
View File

@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './assets/tailwind.css'
import App from './App.vue'
createApp(App).mount('#app')

79
extension/src/style.css Normal file
View File

@@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

7
extension/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client", "chrome"],
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

7
extension/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

12
extension/vite.config.ts Normal file
View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { crx } from '@crxjs/vite-plugin'
import manifest from './manifest.json' assert { type: 'json' }
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
crx({ manifest }),
],
})