From a0648b3a4e24d131669e8a19292c57f9419f6edd Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Fri, 4 Oct 2024 15:27:59 +0300 Subject: [PATCH] feat: poc working --- .editorconfig | 9 ++++ .prettierignore | 1 + .prettierrc | 15 +++++++ eslint.config.mjs | 18 ++++++++ package.json | 1 + pnpm-lock.yaml | 23 +++++++++++ scripts/utils.ts | 10 +++-- src/background/contentScriptHMR.ts | 12 ++++-- src/background/main.ts | 66 +++++++++++++----------------- src/contentScripts/main.tsx | 57 ++++++++++++++------------ src/manifest.ts | 33 ++++++++++++--- 11 files changed, 168 insertions(+), 77 deletions(-) create mode 100644 .editorconfig create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 eslint.config.mjs 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