diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..c1d757f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+[*]
+tab_width = 2
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..e8e450b
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+gen/
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..548c817
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,15 @@
+{
+ "printWidth": 100,
+ "semi": false,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "overrides": [
+ {
+ "files": "*.md",
+ "options": {
+ "printWidth": 100,
+ "proseWrap": "always"
+ }
+ }
+ ]
+}
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..06298dd
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,18 @@
+import eslint from '@eslint/js'
+import tseslint from 'typescript-eslint'
+
+export default [
+ ...tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended),
+ {
+ rules: {
+ 'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
+ '@typescript-eslint/no-unused-vars': [
+ 'warn',
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
+ ],
+ },
+ },
+ {
+ ignores: ['node_modules/', 'build/', 'dist/', 'gen/'],
+ },
+]
diff --git a/package.json b/package.json
index 0e24791..4f53952 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
"prettier": "^3.3.3",
"rimraf": "^6.0.1",
"typescript": "^5.6.2",
+ "typescript-eslint": "^8.8.0",
"unplugin-auto-import": "^0.18.3",
"vite": "^5.4.8",
"web-ext": "^8.3.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fcf9d90..dc47d13 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -87,6 +87,9 @@ importers:
typescript:
specifier: ^5.6.2
version: 5.6.2
+ typescript-eslint:
+ specifier: ^8.8.0
+ version: 8.8.0(eslint@9.11.1)(typescript@5.6.2)
unplugin-auto-import:
specifier: ^0.18.3
version: 0.18.3(rollup@4.24.0)
@@ -2978,6 +2981,15 @@ packages:
typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
+ typescript-eslint@8.8.0:
+ resolution: {integrity: sha512-BjIT/VwJ8+0rVO01ZQ2ZVnjE1svFBiRczcpr1t1Yxt7sT25VSbPfrJtDsQ8uQTy2pilX5nI9gwxhUyLULNentw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
typescript@5.6.2:
resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==}
engines: {node: '>=14.17'}
@@ -6323,6 +6335,17 @@ snapshots:
typedarray@0.0.6: {}
+ typescript-eslint@8.8.0(eslint@9.11.1)(typescript@5.6.2):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.8.0(@typescript-eslint/parser@8.8.0(eslint@9.11.1)(typescript@5.6.2))(eslint@9.11.1)(typescript@5.6.2)
+ '@typescript-eslint/parser': 8.8.0(eslint@9.11.1)(typescript@5.6.2)
+ '@typescript-eslint/utils': 8.8.0(eslint@9.11.1)(typescript@5.6.2)
+ optionalDependencies:
+ typescript: 5.6.2
+ transitivePeerDependencies:
+ - eslint
+ - supports-color
+
typescript@5.6.2: {}
ufo@1.5.4: {}
diff --git a/scripts/utils.ts b/scripts/utils.ts
index 765af1a..7bddb92 100644
--- a/scripts/utils.ts
+++ b/scripts/utils.ts
@@ -1,16 +1,20 @@
+// var process
import { resolve } from 'path'
import { bgCyan, black } from 'kolorist'
import react from '@vitejs/plugin-react'
const __dirname = import.meta.dirname
+console.debug('process', global)
+// process = global.process || {}
+const env = import.meta.env || {}
+// const env = process.env || import.meta.env || {}
export const fastRefresh = true
-export const port = parseInt(process.env.PORT || '') || 3303
+export const port = parseInt(env.PORT || '') || 3303
export const r = (...args: string[]) => resolve(__dirname, '..', ...args)
-export const isDev = process.env.NODE_ENV !== 'production'
+export const isDev = env.NODE_ENV !== 'production'
export const preambleCode = react.preambleCode.replace('__BASE__', '/')
export function log(name: string, message: string) {
- // eslint-disable-next-line no-console
console.log(black(bgCyan(` ${name} `)), message)
}
diff --git a/src/background/contentScriptHMR.ts b/src/background/contentScriptHMR.ts
index a49e848..b8de779 100644
--- a/src/background/contentScriptHMR.ts
+++ b/src/background/contentScriptHMR.ts
@@ -2,17 +2,21 @@ import { isFirefox, isForbiddenUrl } from '@/env'
// Firefox fetch files from cache instead of reloading changes from disk,
// hmr will not work as Chromium based browser
-browser.webNavigation.onCommitted.addListener(({ tabId, frameId, url }) => {
+browser.webNavigation.onCommitted.addListener(async ({ tabId, frameId, url }) => {
// Filter out non main window events.
if (frameId !== 0) return
if (isForbiddenUrl(url)) return
+ console.debug('Injecting', `${isFirefox ? '' : './'}dist/contentScripts/index.global.js`)
+
// inject the latest scripts
- browser.tabs
- .executeScript(tabId, {
+ try {
+ await browser.tabs.executeScript(tabId, {
file: `${isFirefox ? '' : '.'}/dist/contentScripts/index.global.js`,
runAt: 'document_end',
})
- .catch(error => console.error(error))
+ } catch (error) {
+ console.error(error)
+ }
})
diff --git a/src/background/main.ts b/src/background/main.ts
index 405f11a..eaaf3ee 100644
--- a/src/background/main.ts
+++ b/src/background/main.ts
@@ -1,5 +1,5 @@
-import { sendMessage, onMessage } from 'webext-bridge'
-import type { Tabs } from 'webextension-polyfill'
+import { sendMessage } from 'webext-bridge/background'
+import { Tabs } from 'webextension-polyfill'
// only on dev mode
if (import.meta.hot) {
@@ -10,47 +10,37 @@ if (import.meta.hot) {
}
browser.runtime.onInstalled.addListener((): void => {
- // eslint-disable-next-line no-console
console.log('Extension installed')
})
-let previousTabId = 0
+console.log('Hello from background script')
-// communication example: send previous tab title from background page
-// see shim.d.ts for type declaration
-browser.tabs.onActivated.addListener(async ({ tabId }) => {
- if (!previousTabId) {
- previousTabId = tabId
- return
- }
+browser.commands.onCommand.addListener(async (cmd) => {
+ const tab = await browser.tabs
+ .query({ active: true, currentWindow: true })
+ .then((tabs) => tabs[0])
+ console.debug('command received', cmd, tab)
+ const resp = getResponse(cmd, tab)
+ if (!resp || !tab) return
- let tab: Tabs.Tab
-
- try {
- tab = await browser.tabs.get(previousTabId)
- previousTabId = tabId
- } catch {
- return
- }
-
- // eslint-disable-next-line no-console
- console.log('previous tab', tab)
- sendMessage(
- 'tab-prev',
- { title: tab.title },
- { context: 'content-script', tabId },
- )
+ console.debug('sending message', resp, 'to tab', tab.id)
+ return sendMessage(resp.cmd, resp.payload!, {
+ context: 'content-script',
+ tabId: tab.id!,
+ })
})
-onMessage('get-current-tab', async () => {
- try {
- const tab = await browser.tabs.get(previousTabId)
- return {
- title: tab?.title,
- }
- } catch {
- return {
- title: undefined,
- }
+function getResponse(msg: string, tab: Tabs.Tab) {
+ switch (msg) {
+ case 'copy-tab-url':
+ // copy tab url to clipboard
+ return { cmd: 'copy-text', payload: tab?.url }
+ case 'copy-tab-markdown':
+ // copy tab url and title in markdown format
+ return {
+ cmd: 'copy-text',
+ payload: `[${tab?.title}](${tab?.url})`,
+ }
}
-})
+ return null
+}
diff --git a/src/contentScripts/main.tsx b/src/contentScripts/main.tsx
index c4dbcb5..b488196 100644
--- a/src/contentScripts/main.tsx
+++ b/src/contentScripts/main.tsx
@@ -1,30 +1,35 @@
-import App from './App'
-import * as ReactDOM from 'react-dom/client'
+// import App from './App'
+// import * as ReactDOM from 'react-dom/client'
import { onMessage } from 'webext-bridge/content-script'
-// Firefox `browser.tabs.executeScript()` requires scripts return a primitive value
-;(() => {
- console.info('[vite-react-webext] Hello world from content script')
+ // Firefox `browser.tabs.executeScript()` requires scripts return a primitive value
+ ; (() => {
+ console.info('[vite-react-webext] Hello world from content script')
- // communication example: send previous tab title from background page
- onMessage('tab-prev', ({ data }) => {
- console.log(`[vite-react-webext] Navigate from page "${data.title}"`)
- })
+ onMessage('copy-text', (payload) => {
+ console.debug('copy-text', payload)
+ setTimeout(async () => {
+ console.debug('Copying text', payload.data)
+ navigator.clipboard.writeText(payload.data as string)
+ }, 10)
+ // document.execCommand('copy', false, payload.data as string)
+ })
- // mount component to context window
- const container = document.createElement('div')
- const root = document.createElement('div')
- const styleEl = document.createElement('link')
- const shadowDOM =
- container.attachShadow?.({ mode: __DEV__ ? 'open' : 'closed' }) || container
- styleEl.setAttribute('rel', 'stylesheet')
- styleEl.setAttribute(
- 'href',
- browser.runtime.getURL('dist/contentScripts/style.css'),
- )
- shadowDOM.appendChild(styleEl)
- shadowDOM.appendChild(root)
- document.body.appendChild(container)
- const reactRoot = ReactDOM.createRoot(root)
- reactRoot.render()
-})()
+ // mount component to context window
+ try {
+ // const container = document.createElement('div')
+ // const root = document.createElement('div')
+ // const styleEl = document.createElement('link')
+ // const shadowDOM = container.attachShadow?.({ mode: __DEV__ ? 'open' : 'closed' }) || container
+ // styleEl.setAttribute('rel', 'stylesheet')
+ // styleEl.setAttribute('href', browser.runtime.getURL('dist/contentScripts/style.css'))
+ // shadowDOM.appendChild(styleEl)
+ // shadowDOM.appendChild(root)
+ // document.body.appendChild(container)
+ // const reactRoot = ReactDOM.createRoot(root)
+ // reactRoot.render()
+ console.info('[vite-react-webext] Component mounted')
+ } catch (error) {
+ console.error('Error mounting component', error)
+ }
+ })()
diff --git a/src/manifest.ts b/src/manifest.ts
index 1d17061..b3dd1e3 100644
--- a/src/manifest.ts
+++ b/src/manifest.ts
@@ -32,7 +32,15 @@ export async function getManifest() {
48: './assets/icon-512.png',
128: './assets/icon-512.png',
},
- permissions: ['tabs', 'storage', 'activeTab', 'http://*/', 'https://*/'],
+ permissions: [
+ // 'host',
+ 'tabs',
+ 'storage',
+ 'activeTab',
+ 'clipboardWrite',
+ 'http://*/',
+ 'https://*/',
+ ],
content_scripts: [
{
matches: ['http://*/*', 'https://*/*'],
@@ -40,6 +48,22 @@ export async function getManifest() {
},
],
web_accessible_resources: ['dist/contentScripts/style.css'],
+ commands: {
+ 'copy-tab-url': {
+ suggested_key: {
+ default: 'Ctrl+Shift+U',
+ mac: 'Command+Shift+U',
+ },
+ description: 'Copy the current tab URL',
+ },
+ 'copy-tab-markdown': {
+ suggested_key: {
+ default: 'Ctrl+Shift+M',
+ mac: 'Command+Shift+M',
+ },
+ description: 'Copy the current tab URL and title in Markdown format',
+ },
+ },
}
if (isDev) {
@@ -49,13 +73,10 @@ export async function getManifest() {
delete manifest.content_scripts
manifest.permissions?.push('webNavigation')
- const preambleCodeHash = crypto
- .createHash('sha256')
- .update(preambleCode)
- .digest('base64')
+ const preambleCodeHash = crypto.createHash('sha256').update(preambleCode).digest('base64')
// this is required on dev for Vite script to load
- manifest.content_security_policy = `script-src \'self\' 'sha256-${preambleCodeHash}' http://localhost:${port}; object-src \'self\'`
+ manifest.content_security_policy = `script-src 'self' 'sha256-${preambleCodeHash}' http://localhost:${port}; object-src 'self'`
}
return manifest