├── .eslintrc ├── src ├── logic │ ├── index.ts │ ├── storage.ts │ └── common-setup.ts ├── styles │ ├── index.ts │ └── main.css ├── components │ ├── SharedSubtitle.vue │ ├── Logo.vue │ ├── __tests__ │ │ └── Logo.test.ts │ └── README.md ├── monaco-editor │ ├── index.ts │ ├── suggestion.ts │ ├── index.html │ └── MonacoEditorIframe.ts ├── options │ ├── Options.vue │ ├── main.ts │ └── index.html ├── tests │ └── demo.spec.ts ├── global.d.ts ├── popup │ ├── main.ts │ ├── Popup.vue │ └── index.html ├── background │ ├── index.html │ ├── contentScriptHMR.ts │ └── main.ts ├── assets │ └── icon.svg ├── env.ts ├── types │ └── iframeMessage.ts ├── composables │ ├── useTheme.ts │ └── useStorageLocal.ts ├── contentScripts │ ├── views │ │ ├── App.vue │ │ ├── CssEditor.vue │ │ └── Logo.vue │ ├── utils.ts │ └── index.ts └── manifest.ts ├── .eslintignore ├── .npmrc ├── assets ├── icon-16.png ├── icon-48.png ├── icon-128.png └── icon-512.png ├── images └── screenshot.png ├── .vscode ├── extensions.json └── settings.json ├── modules.d.ts ├── scripts ├── manifest.ts ├── utils.ts └── prepare.ts ├── .gitignore ├── unocss.config.ts ├── shim.d.ts ├── playwright.config.ts ├── tsconfig.json ├── vite.config.monaco.ts ├── extension └── monaco-editor │ └── iframe │ ├── index.html │ └── index.js ├── extension-firefox └── monaco-editor │ └── iframe │ ├── index.html │ └── index.js ├── e2e ├── basic.spec.ts └── fixtures.ts ├── LICENSE ├── vite.config.background.ts ├── vite.config.content.ts ├── README.md ├── vite.config.ts └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu" 3 | } 4 | -------------------------------------------------------------------------------- /src/logic/index.ts: -------------------------------------------------------------------------------- 1 | export * from './storage' 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | public 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | auto-install-peers=true 3 | -------------------------------------------------------------------------------- /assets/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdgg/xLog-custom-css-debugger/HEAD/assets/icon-16.png -------------------------------------------------------------------------------- /assets/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdgg/xLog-custom-css-debugger/HEAD/assets/icon-48.png -------------------------------------------------------------------------------- /assets/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdgg/xLog-custom-css-debugger/HEAD/assets/icon-128.png -------------------------------------------------------------------------------- /assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdgg/xLog-custom-css-debugger/HEAD/assets/icon-512.png -------------------------------------------------------------------------------- /src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import '@unocss/reset/tailwind.css' 2 | import './main.css' 3 | import 'uno.css' 4 | -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdgg/xLog-custom-css-debugger/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /src/logic/storage.ts: -------------------------------------------------------------------------------- 1 | // export const storageDemo = useStorageLocal('webext-demo', 'Storage Demo') 2 | export const storageDemo = {} 3 | -------------------------------------------------------------------------------- /src/components/SharedSubtitle.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/monaco-editor/index.ts: -------------------------------------------------------------------------------- 1 | import MonacoEditorIframe from './MonacoEditorIframe' 2 | // eslint-disable-next-line no-new 3 | new MonacoEditorIframe() 4 | -------------------------------------------------------------------------------- /src/options/Options.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /src/tests/demo.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | describe('Demo', () => { 4 | it('should work', () => { 5 | expect(1 + 1).toBe(2) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vue.volar", 4 | "antfu.iconify", 5 | "antfu.unocss", 6 | "dbaeumer.vscode-eslint", 7 | "csstools.postcss" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare const __DEV__: boolean 2 | /** Extension name, defined in packageJson.name */ 3 | declare const __NAME__: string 4 | 5 | declare module '*.vue' { 6 | const component: any 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /src/popup/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './Popup.vue' 3 | import { setupApp } from '~/logic/common-setup' 4 | import '../styles' 5 | 6 | const app = createApp(App) 7 | setupApp(app) 8 | app.mount('#app') 9 | -------------------------------------------------------------------------------- /src/options/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './Options.vue' 3 | import { setupApp } from '~/logic/common-setup' 4 | import '../styles' 5 | 6 | const app = createApp(App) 7 | setupApp(app) 8 | app.mount('#app') 9 | -------------------------------------------------------------------------------- /modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@vue/runtime-core' { 2 | interface ComponentCustomProperties { 3 | $app: { 4 | context: string 5 | } 6 | } 7 | } 8 | 9 | // https://stackoverflow.com/a/64189046/479957 10 | export {} 11 | -------------------------------------------------------------------------------- /src/popup/Popup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Options 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["Vitesse"], 3 | "typescript.tsdk": "node_modules/typescript/lib", 4 | "vite.autoStart": false, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": true 7 | }, 8 | "files.associations": { 9 | "*.css": "postcss" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Popup 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/background/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Background 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/__tests__/Logo.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { mount } from '@vue/test-utils' 3 | import Logo from '../Logo.vue' 4 | 5 | describe('Logo Component', () => { 6 | it('should render', () => { 7 | const wrapper = mount(Logo) 8 | 9 | expect(wrapper.html()).toBeTruthy() 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /scripts/manifest.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import { getManifest } from '../src/manifest' 3 | import { log, outputDir, r } from './utils' 4 | 5 | export async function writeManifest() { 6 | await fs.writeJSON(r(`${outputDir}/manifest.json`), await getManifest(), { spaces: 2 }) 7 | log('PRE', 'write manifest.json') 8 | } 9 | 10 | writeManifest() 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .vite-ssg-dist 4 | .vite-ssg-temp 5 | *.crx 6 | *.local 7 | *.log 8 | *.pem 9 | *.xpi 10 | *.zip 11 | dist 12 | dist-ssr 13 | extension/manifest.json 14 | extension/assets 15 | extension-firefox/manifest.json 16 | extension-firefox/assets 17 | node_modules 18 | src/auto-imports.d.ts 19 | src/components.d.ts 20 | .eslintcache 21 | -------------------------------------------------------------------------------- /src/monaco-editor/suggestion.ts: -------------------------------------------------------------------------------- 1 | // TODO: support document 2 | export const selectorSuggestions = [ 3 | '.xlog-header', 4 | '.xlog-site-info', 5 | '.xlog-user', 6 | '.xlog-banner', 7 | '.xlog-post-title', 8 | '.xlog-post-meta', 9 | '.xlog-post-summary', 10 | 'prose', 11 | '.xlog-post-toc', 12 | '.xlog-reactions', 13 | '.xlog-comment', 14 | ] 15 | -------------------------------------------------------------------------------- /unocss.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'unocss/vite' 2 | import { presetAttributify, presetIcons, presetUno, transformerDirectives } from 'unocss' 3 | 4 | export default defineConfig({ 5 | presets: [ 6 | presetUno(), 7 | presetAttributify(), 8 | presetIcons(), 9 | ], 10 | transformers: [ 11 | transformerDirectives(), 12 | ], 13 | }) 14 | -------------------------------------------------------------------------------- /src/assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /shim.d.ts: -------------------------------------------------------------------------------- 1 | import type { ProtocolWithReturn } from 'webext-bridge' 2 | 3 | declare module 'webext-bridge' { 4 | export interface ProtocolMap { 5 | // define message protocol types 6 | // see https://github.com/antfu/webext-bridge#type-safe-protocols 7 | 'tab-prev': { title: string | undefined } 8 | 'get-current-tab': ProtocolWithReturn<{ tabId: number }, { title?: string }> 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | const forbiddenProtocols = [ 2 | 'chrome-extension://', 3 | 'chrome-search://', 4 | 'chrome://', 5 | 'devtools://', 6 | 'edge://', 7 | 'https://chrome.google.com/webstore', 8 | ] 9 | 10 | export function isForbiddenUrl(url: string): boolean { 11 | return forbiddenProtocols.some(protocol => url.startsWith(protocol)) 12 | } 13 | 14 | export const isFirefox = navigator.userAgent.includes('Firefox') 15 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see {@link https://playwright.dev/docs/chrome-extensions Chrome extensions | Playwright} 3 | */ 4 | import { defineConfig } from '@playwright/test' 5 | 6 | export default defineConfig({ 7 | testDir: './e2e', 8 | retries: 2, 9 | webServer: { 10 | command: 'npm run dev', 11 | // start e2e test after the Vite server is fully prepared 12 | url: 'http://localhost:3303/popup/main.ts', 13 | reuseExistingServer: true, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/README.md: -------------------------------------------------------------------------------- 1 | ## Components 2 | 3 | Components in this dir will be auto-registered and on-demand, powered by [`vite-plugin-components`](https://github.com/antfu/vite-plugin-components). 4 | 5 | Components can be shared in all views. 6 | 7 | ### Icons 8 | 9 | You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/). 10 | 11 | It will only bundle the icons you use. Check out [vite-plugin-icons](https://github.com/antfu/vite-plugin-icons) for more details. 12 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #app { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | .btn { 9 | @apply px-4 py-1 rounded inline-block 10 | bg-teal-600 text-white cursor-pointer 11 | hover:bg-teal-700 12 | disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50; 13 | } 14 | 15 | .icon-btn { 16 | @apply inline-block cursor-pointer select-none 17 | opacity-75 transition duration-200 ease-in-out 18 | hover:opacity-100 hover:text-teal-600; 19 | font-size: 0.9em; 20 | } 21 | -------------------------------------------------------------------------------- /scripts/utils.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { bgCyan, black } from 'kolorist' 3 | 4 | export const port = parseInt(process.env.PORT || '') || 3303 5 | export const r = (...args: string[]) => resolve(__dirname, '..', ...args) 6 | export const isDev = process.env.NODE_ENV !== 'production' 7 | export const isFirefox = process.env.EXTENSION === 'firefox' 8 | export const outputDir = isFirefox ? 'extension-firefox' : 'extension' 9 | 10 | export function log(name: string, message: string) { 11 | console.log(black(bgCyan(` ${name} `)), message) 12 | } 13 | -------------------------------------------------------------------------------- /src/types/iframeMessage.ts: -------------------------------------------------------------------------------- 1 | export interface IframeCssUpdatedMessage { 2 | type: 'xlogMonacoIframeCssUpdated' 3 | css: string 4 | } 5 | 6 | export interface IframeLoadedMessage { 7 | type: 'xlogMonacoIframeLoaded' 8 | } 9 | 10 | export type IframeMessage = IframeCssUpdatedMessage | IframeLoadedMessage 11 | 12 | export interface ParentThemeChange { 13 | type: 'xlogThemeChange' 14 | theme: 'light' | 'dark' 15 | } 16 | 17 | export interface ParentCssMessage { 18 | type: 'xlogCssInit' 19 | css: string 20 | } 21 | 22 | export type ParentMessage = ParentThemeChange | ParentCssMessage 23 | -------------------------------------------------------------------------------- /src/logic/common-setup.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | 3 | export function setupApp(app: App) { 4 | // Inject a globally available `$app` object in template 5 | app.config.globalProperties.$app = { 6 | context: '', 7 | } 8 | 9 | // Provide access to `app` in script setup with `const app = inject('app')` 10 | app.provide('app', app.config.globalProperties.$app) 11 | 12 | // Here you can install additional plugins for all contexts: popup, options page and content-script. 13 | // example: app.use(i18n) 14 | // example excluding content-script context: if (context !== 'content-script') app.use(i18n) 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "es2016", 6 | "lib": ["DOM", "ESNext"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "incremental": false, 10 | "skipLibCheck": true, 11 | "jsx": "preserve", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "noUnusedLocals": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "types": [ 17 | "vite/client" 18 | ], 19 | "paths": { 20 | "~/*": ["src/*"] 21 | } 22 | }, 23 | "exclude": ["dist", "node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /vite.config.monaco.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { isDev, outputDir, r } from './scripts/utils' 3 | 4 | export default defineConfig({ 5 | root: r('src'), 6 | resolve: { 7 | alias: { 8 | '~/': `${r('src')}/`, 9 | }, 10 | }, 11 | build: { 12 | watch: isDev 13 | ? {} 14 | : undefined, 15 | outDir: r(`${outputDir}/monaco-editor/iframe`), 16 | assetsDir: '', 17 | emptyOutDir: false, 18 | sourcemap: false, 19 | rollupOptions: { 20 | input: r('src/monaco-editor/index.ts'), 21 | output: { 22 | entryFileNames: 'index.js', 23 | }, 24 | }, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /src/background/contentScriptHMR.ts: -------------------------------------------------------------------------------- 1 | import { isFirefox, isForbiddenUrl } from '~/env' 2 | 3 | // Firefox fetch files from cache instead of reloading changes from disk, 4 | // hmr will not work as Chromium based browser 5 | browser.webNavigation.onCommitted.addListener(({ tabId, frameId, url }) => { 6 | // Filter out non main window events. 7 | if (frameId !== 0) 8 | return 9 | 10 | if (isForbiddenUrl(url)) 11 | return 12 | 13 | // inject the latest scripts 14 | browser.tabs.executeScript(tabId, { 15 | file: `${isFirefox ? '' : '.'}/dist/contentScripts/index.global.js`, 16 | runAt: 'document_end', 17 | }).catch(error => console.error(error)) 18 | }) 19 | -------------------------------------------------------------------------------- /src/composables/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { onBeforeUnmount, onMounted, ref } from 'vue' 2 | type Theme = 'light' | 'dark' 3 | 4 | export function useTheme() { 5 | const theme = ref('light') 6 | 7 | const observer = new MutationObserver((mutationsList) => { 8 | mutationsList.forEach((mutation) => { 9 | if (mutation.attributeName === 'class') 10 | theme.value = mutation.target.className 11 | }) 12 | }) 13 | 14 | onMounted(() => { 15 | theme.value = document.documentElement.className as Theme 16 | observer.observe(document.documentElement, { attributes: true }) 17 | }) 18 | 19 | onBeforeUnmount(() => { 20 | observer.disconnect() 21 | }) 22 | 23 | return theme 24 | } 25 | -------------------------------------------------------------------------------- /src/background/main.ts: -------------------------------------------------------------------------------- 1 | import { sendMessage } from 'webext-bridge/background' 2 | 3 | // only on dev mode 4 | if (import.meta.hot) { 5 | // @ts-expect-error for background HMR 6 | import('/@vite/client') 7 | // load latest content script 8 | import('./contentScriptHMR') 9 | } 10 | 11 | browser.runtime.onInstalled.addListener((): void => { 12 | 13 | }) 14 | 15 | // const previousTabId = 0 16 | 17 | browser.contextMenus.create({ 18 | id: 'toggleComponent', 19 | title: 'Toggle xLog css debugger', 20 | contexts: ['all'], 21 | }) 22 | 23 | browser.contextMenus.onClicked.addListener((info, tab) => { 24 | if (info.menuItemId === 'toggleComponent' && tab?.id) 25 | sendMessage('toggleComponent', {}, `content-script@${tab.id}`) 26 | }) 27 | -------------------------------------------------------------------------------- /src/contentScripts/views/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | -------------------------------------------------------------------------------- /src/monaco-editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /extension/monaco-editor/iframe/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /extension-firefox/monaco-editor/iframe/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/composables/useStorageLocal.ts: -------------------------------------------------------------------------------- 1 | import { storage } from 'webextension-polyfill' 2 | import type { 3 | MaybeRef, 4 | RemovableRef, 5 | StorageLikeAsync, 6 | UseStorageAsyncOptions, 7 | } from '@vueuse/core' 8 | import { 9 | useStorageAsync, 10 | } from '@vueuse/core' 11 | 12 | const storageLocal: StorageLikeAsync = { 13 | removeItem(key: string) { 14 | return storage.local.remove(key) 15 | }, 16 | 17 | setItem(key: string, value: string) { 18 | return storage.local.set({ [key]: value }) 19 | }, 20 | 21 | async getItem(key: string) { 22 | return (await storage.local.get(key))[key] 23 | }, 24 | } 25 | 26 | export const useStorageLocal = ( 27 | key: string, 28 | initialValue: MaybeRef, 29 | options?: UseStorageAsyncOptions, 30 | ): RemovableRef => useStorageAsync(key, initialValue, storageLocal, options) 31 | -------------------------------------------------------------------------------- /e2e/basic.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, isDevArtifact, name, test } from './fixtures' 2 | 3 | test('example test', async ({ page }, testInfo) => { 4 | testInfo.skip(!isDevArtifact(), 'contentScript is in closed ShadowRoot mode') 5 | 6 | await page.goto('https://example.com') 7 | 8 | await page.locator(`#${name} button`).click() 9 | await expect(page.locator(`#${name} h1`)).toHaveText('Vitesse WebExt') 10 | }) 11 | 12 | test('popup page', async ({ page, extensionId }) => { 13 | await page.goto(`chrome-extension://${extensionId}/dist/popup/index.html`) 14 | await expect(page.locator('button')).toHaveText('Open Options') 15 | }) 16 | 17 | test('options page', async ({ page, extensionId }) => { 18 | await page.goto(`chrome-extension://${extensionId}/dist/options/index.html`) 19 | await expect(page.locator('img')).toHaveAttribute('alt', 'extension icon') 20 | }) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 birdgg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vite.config.background.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { sharedConfig } from './vite.config' 3 | import { isDev, outputDir, r } from './scripts/utils' 4 | import packageJson from './package.json' 5 | 6 | // bundling the content script using Vite 7 | export default defineConfig({ 8 | ...sharedConfig, 9 | define: { 10 | '__DEV__': isDev, 11 | '__NAME__': JSON.stringify(packageJson.name), 12 | // https://github.com/vitejs/vite/issues/9320 13 | // https://github.com/vitejs/vite/issues/9186 14 | 'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'), 15 | }, 16 | build: { 17 | watch: isDev 18 | ? {} 19 | : undefined, 20 | outDir: r(`${outputDir}/dist/background`), 21 | cssCodeSplit: false, 22 | emptyOutDir: false, 23 | sourcemap: isDev ? 'inline' : false, 24 | lib: { 25 | entry: r('src/background/main.ts'), 26 | name: packageJson.name, 27 | formats: ['iife'], 28 | }, 29 | rollupOptions: { 30 | output: { 31 | entryFileNames: 'index.mjs', 32 | extend: true, 33 | }, 34 | }, 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /vite.config.content.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { sharedConfig } from './vite.config' 3 | import { isDev, outputDir, r } from './scripts/utils' 4 | import packageJson from './package.json' 5 | 6 | // bundling the content script using Vite 7 | export default defineConfig({ 8 | ...sharedConfig, 9 | define: { 10 | '__DEV__': isDev, 11 | '__NAME__': JSON.stringify(packageJson.name), 12 | // https://github.com/vitejs/vite/issues/9320 13 | // https://github.com/vitejs/vite/issues/9186 14 | 'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'), 15 | }, 16 | build: { 17 | watch: isDev 18 | ? {} 19 | : undefined, 20 | outDir: r(`${outputDir}/dist/contentScripts`), 21 | cssCodeSplit: false, 22 | emptyOutDir: false, 23 | sourcemap: isDev ? 'inline' : false, 24 | lib: { 25 | entry: r('src/contentScripts/index.ts'), 26 | name: packageJson.name, 27 | formats: ['iife'], 28 | }, 29 | rollupOptions: { 30 | output: { 31 | entryFileNames: 'index.global.js', 32 | extend: true, 33 | }, 34 | }, 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /src/contentScripts/utils.ts: -------------------------------------------------------------------------------- 1 | import { decode, encode } from 'js-base64' 2 | 3 | export function removeDebuggerNode() { 4 | const debuggerNode = document.querySelector( 5 | '#xlog-custom-css-debugger', 6 | ) as HTMLElement 7 | debuggerNode.remove() 8 | } 9 | 10 | export function getCss() { 11 | const stylesheet = document 12 | .querySelector('.xlog-user link[rel="stylesheet"]') 13 | ?.getAttribute('href') 14 | if (stylesheet) { 15 | const regex = /data:text\/css;base64,(.*)/ 16 | const base64String = stylesheet.match(regex)?.[1] || '' 17 | const cssString = decode(base64String) 18 | 19 | return cssString 20 | } 21 | else { 22 | return '' 23 | } 24 | } 25 | 26 | export function updateCss(css: string) { 27 | const xlogUserNode = document.querySelector('.xlog-user') as HTMLElement 28 | const stylesheet = document.querySelector( 29 | '.xlog-user link[rel="stylesheet"]', 30 | ) 31 | const base64CssString = `data:text/css;base64,${encode(css)}` 32 | if (stylesheet) { 33 | stylesheet?.setAttribute('href', base64CssString) 34 | } 35 | else { 36 | const link = document.createElement('link') 37 | link.setAttribute('rel', 'stylesheet') 38 | link.setAttribute('href', base64CssString) 39 | xlogUserNode.appendChild(link) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/contentScripts/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { onMessage } from 'webext-bridge/content-script' 3 | import App from './views/App.vue' 4 | import { removeDebuggerNode } from './utils' 5 | import { setupApp } from '~/logic/common-setup' 6 | 7 | // Firefox `browser.tabs.executeScript()` requires scripts return a primitive value 8 | (() => { 9 | // communication example: send previous tab title from background page 10 | // onMessage('tab-prev', ({ data }) => { 11 | // console.log(`[vitesse-webext] Navigate from page "${data.title}"`) 12 | // }) 13 | 14 | // mount component to context window 15 | onMessage('toggleComponent', () => { 16 | if (document.querySelector(`#${__NAME__}`)) { 17 | removeDebuggerNode() 18 | return 19 | } 20 | const container = document.createElement('div') 21 | container.id = __NAME__ 22 | const root = document.createElement('div') 23 | const styleEl = document.createElement('link') 24 | const shadowDOM = container.attachShadow?.({ mode: __DEV__ ? 'open' : 'closed' }) || container 25 | styleEl.setAttribute('rel', 'stylesheet') 26 | styleEl.setAttribute('href', browser.runtime.getURL('dist/contentScripts/style.css')) 27 | shadowDOM.appendChild(styleEl) 28 | shadowDOM.appendChild(root) 29 | document.body.appendChild(container) 30 | const app = createApp(App) 31 | setupApp(app) 32 | app.mount(root) 33 | }) 34 | })() 35 | -------------------------------------------------------------------------------- /e2e/fixtures.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { setTimeout as sleep } from 'node:timers/promises' 3 | import fs from 'fs-extra' 4 | import { type BrowserContext, test as base, chromium } from '@playwright/test' 5 | import type { Manifest } from 'webextension-polyfill' 6 | 7 | export { name } from '../package.json' 8 | 9 | export const extensionPath = path.join(__dirname, '../extension') 10 | 11 | export const test = base.extend<{ 12 | context: BrowserContext 13 | extensionId: string 14 | }>({ 15 | context: async ({ headless }, use) => { 16 | // workaround for the Vite server has started but contentScript is not yet. 17 | await sleep(1000) 18 | const context = await chromium.launchPersistentContext('', { 19 | headless, 20 | args: [ 21 | ...(headless ? ['--headless=new'] : []), 22 | `--disable-extensions-except=${extensionPath}`, 23 | `--load-extension=${extensionPath}`, 24 | ], 25 | }) 26 | await use(context) 27 | await context.close() 28 | }, 29 | extensionId: async ({ context }, use) => { 30 | // for manifest v3: 31 | let [background] = context.serviceWorkers() 32 | if (!background) 33 | background = await context.waitForEvent('serviceworker') 34 | 35 | const extensionId = background.url().split('/')[2] 36 | await use(extensionId) 37 | }, 38 | }) 39 | 40 | export const expect = test.expect 41 | 42 | export function isDevArtifact() { 43 | const manifest: Manifest.WebExtensionManifest = fs.readJsonSync(path.resolve(extensionPath, 'manifest.json')) 44 | return Boolean( 45 | typeof manifest.content_security_policy === 'object' 46 | && manifest.content_security_policy.extension_pages?.includes('localhost'), 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xLog custom css debugger 2 | 3 |

4 | 5 | MIT License 6 | 7 | 8 | GitHub package.json version 9 | 10 | 11 | Chrome 12 | 13 | 14 | 15 | Firefox 16 | 17 | 18 | 19 | Microsoft Edge 20 | 21 |

22 | 23 | A convenient web extension for debugging custom CSS in xLog. 24 | 25 | ![screenshot](./images/screenshot.png) 26 | 27 | ## Features 28 | - Debug your xlog custom css in real time 29 | - Enjoy a pleasant code editing experience with the Monaco Editor 30 | - Explore other fantastic CSS styles on the fly 31 | 32 | ## How to use 33 | 34 | on your page, right click and select Toggle xLog css debugger to open editor 35 | 36 | enjoy your debugging 37 | ## Develop 38 | 39 | ``` 40 | $ pnpm i 41 | 42 | // chrome 43 | $ pnpm dev 44 | 45 | // firefox 46 | $ pnpm dev-firefox 47 | $ pnpm start:firefox 48 | ``` 49 | ## TODO 50 | - [ ] modify style 51 | - [ ] setup ci 52 | 53 | ## Contribute 54 | 55 | fork from `dev` branch, and submit your pr to `dev` -------------------------------------------------------------------------------- /extension/monaco-editor/iframe/index.js: -------------------------------------------------------------------------------- 1 | const o=[".xlog-header",".xlog-site-info",".xlog-user",".xlog-banner",".xlog-post-title",".xlog-post-meta",".xlog-post-summary","prose",".xlog-post-toc",".xlog-reactions",".xlog-comment"];class i{constructor(){this.theme="vs",this.loadEditor(()=>{this.attachWindowListeners(),this.initEditor(),this.injectSuggestion(),this.postMessage({type:"xlogMonacoIframeLoaded"})})}loadEditor(e){window.require.config({paths:{vs:browser.runtime.getURL("monaco-editor/iframe/node_modules/monaco-editor/min/vs")}}),window.require(["vs/editor/editor.main"],e)}initEditor(){const e=this.getContainer(),t=this.getEditorOptions();this.editor=window.monaco.editor.create(e,t),this.editor.onDidChangeModelContent(()=>{this.postMessage({css:this.editor.getValue(),type:"xlogMonacoIframeCssUpdated"})})}injectSuggestion(){const e=o.map(t=>({label:t,kind:window.monaco.languages.CompletionItemKind.Snippet,insertText:t,insertTextRules:window.monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet}));window.monaco.languages.registerCompletionItemProvider("css",{provideCompletionItems(){return{triggerCharacters:[".","#"],suggestions:e}}})}getContainer(){return document.getElementById("container")}getEditorOptions(){const e=this.getContainer(),t=Math.round(e.offsetWidth/8);return{value:"",tabSize:2,theme:this.theme,wordWrap:"bounded",wordWrapColumn:t,scrollBeyondLastLine:!1,language:"css",folding:!1,renderLineHighlight:"none",suggestOnTriggerCharacters:!1,cursorBlinking:"smooth",mouseWheelZoom:!1,lineNumbers:"off",minimap:{enabled:!1},hover:{enabled:!1},codeLens:!1}}postMessage(e){window.parent.postMessage(e,"*")}updateTheme(e){const t=e==="light"?"vs":"vs-dark";this.theme=t,window.monaco.editor.setTheme(t)}attachWindowListeners(){window.addEventListener("resize",()=>{this.editor.layout(),this.editor.updateOptions(this.getEditorOptions())}),window.addEventListener("message",e=>{e.data.type==="xlogCssInit"&&this.editor.setValue(e.data.css),e.data.type==="xlogThemeChange"&&this.updateTheme(e.data.theme)})}}new i; 2 | -------------------------------------------------------------------------------- /extension-firefox/monaco-editor/iframe/index.js: -------------------------------------------------------------------------------- 1 | const o=[".xlog-header",".xlog-site-info",".xlog-user",".xlog-banner",".xlog-post-title",".xlog-post-meta",".xlog-post-summary","prose",".xlog-post-toc",".xlog-reactions",".xlog-comment"];class i{constructor(){this.theme="vs",this.loadEditor(()=>{this.attachWindowListeners(),this.initEditor(),this.injectSuggestion(),this.postMessage({type:"xlogMonacoIframeLoaded"})})}loadEditor(e){window.require.config({paths:{vs:browser.runtime.getURL("monaco-editor/iframe/node_modules/monaco-editor/min/vs")}}),window.require(["vs/editor/editor.main"],e)}initEditor(){const e=this.getContainer(),t=this.getEditorOptions();this.editor=window.monaco.editor.create(e,t),this.editor.onDidChangeModelContent(()=>{this.postMessage({css:this.editor.getValue(),type:"xlogMonacoIframeCssUpdated"})})}injectSuggestion(){const e=o.map(t=>({label:t,kind:window.monaco.languages.CompletionItemKind.Snippet,insertText:t,insertTextRules:window.monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet}));window.monaco.languages.registerCompletionItemProvider("css",{provideCompletionItems(){return{triggerCharacters:[".","#"],suggestions:e}}})}getContainer(){return document.getElementById("container")}getEditorOptions(){const e=this.getContainer(),t=Math.round(e.offsetWidth/8);return{value:"",tabSize:2,theme:this.theme,wordWrap:"bounded",wordWrapColumn:t,scrollBeyondLastLine:!1,language:"css",folding:!1,renderLineHighlight:"none",suggestOnTriggerCharacters:!1,cursorBlinking:"smooth",mouseWheelZoom:!1,lineNumbers:"off",minimap:{enabled:!1},hover:{enabled:!1},codeLens:!1}}postMessage(e){window.parent.postMessage(e,"*")}updateTheme(e){const t=e==="light"?"vs":"vs-dark";this.theme=t,window.monaco.editor.setTheme(t)}attachWindowListeners(){window.addEventListener("resize",()=>{this.editor.layout(),this.editor.updateOptions(this.getEditorOptions())}),window.addEventListener("message",e=>{e.data.type==="xlogCssInit"&&this.editor.setValue(e.data.css),e.data.type==="xlogThemeChange"&&this.updateTheme(e.data.theme)})}}new i; 2 | -------------------------------------------------------------------------------- /scripts/prepare.ts: -------------------------------------------------------------------------------- 1 | // generate stub index.html files for dev entry 2 | import { execSync } from 'node:child_process' 3 | import fs from 'fs-extra' 4 | import chokidar from 'chokidar' 5 | import { isDev, log, outputDir, port, r } from './utils' 6 | 7 | /** 8 | * Stub index.html to use Vite in development 9 | */ 10 | async function stubIndexHtml() { 11 | const views = [ 12 | 'options', 13 | 'popup', 14 | 'background', 15 | ] 16 | 17 | for (const view of views) { 18 | await fs.ensureDir(r(`${outputDir}/dist/${view}`)) 19 | let data = await fs.readFile(r(`src/${view}/index.html`), 'utf-8') 20 | data = data 21 | .replace('"./main.ts"', `"http://localhost:${port}/${view}/main.ts"`) 22 | .replace('
', '
Vite server did not start
') 23 | await fs.writeFile(r(`${outputDir}/dist/${view}/index.html`), data, 'utf-8') 24 | log('PRE', `stub ${view}`) 25 | } 26 | } 27 | 28 | function writeManifest() { 29 | execSync('npx esno ./scripts/manifest.ts', { stdio: 'inherit' }) 30 | } 31 | 32 | writeManifest() 33 | 34 | if (isDev) { 35 | stubIndexHtml() 36 | chokidar.watch(r('src/**/*.html')) 37 | .on('change', () => { 38 | stubIndexHtml() 39 | }) 40 | chokidar.watch([r('src/manifest.ts'), r('package.json')]) 41 | .on('change', () => { 42 | writeManifest() 43 | }) 44 | } 45 | 46 | fs.copySync('assets', `${outputDir}/assets`) 47 | // copy monaco editor 48 | fs.copySync( 49 | 'src/monaco-editor/index.html', 50 | `${outputDir}/monaco-editor/iframe/index.html`) 51 | fs.copySync('node_modules/webextension-polyfill/dist/browser-polyfill.min.js', 52 | `${outputDir}/monaco-editor/iframe/node_modules/browser-polyfill.min.js`, 53 | ) 54 | fs.copySync( 55 | 'node_modules/requirejs/require.js', 56 | `${outputDir}/monaco-editor/iframe/node_modules/requirejs/require.js`) 57 | fs.copySync( 58 | 'node_modules/monaco-editor/min', 59 | `${outputDir}/monaco-editor/iframe/node_modules/monaco-editor/min`) 60 | fs.removeSync(`${outputDir}/monaco-editor/iframe/node_modules/monaco-editor/min/vs/language/typescript`) 61 | fs.removeSync(`${outputDir}/monaco-editor/iframe/node_modules/monaco-editor/min/vs/language/json`) 62 | fs.removeSync(`${outputDir}/monaco-editor/iframe/node_modules/monaco-editor/min/vs/language/html`) 63 | -------------------------------------------------------------------------------- /src/manifest.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import type { Manifest } from 'webextension-polyfill' 3 | import type PkgType from '../package.json' 4 | import { isDev, isFirefox, port, r } from '../scripts/utils' 5 | 6 | export async function getManifest() { 7 | const pkg = await fs.readJSON(r('package.json')) as typeof PkgType 8 | 9 | // update this file to update this manifest.json 10 | // can also be conditional based on your need 11 | const manifest: Manifest.WebExtensionManifest = { 12 | manifest_version: 3, 13 | name: pkg.displayName || pkg.name, 14 | version: pkg.version, 15 | description: pkg.description, 16 | action: { 17 | default_icon: './assets/icon-512.png', 18 | // default_popup: './dist/popup/index.html', 19 | }, 20 | // options_ui: { 21 | // page: './dist/options/index.html', 22 | // open_in_tab: true, 23 | // }, 24 | background: isFirefox 25 | ? { 26 | scripts: ['dist/background/index.mjs'], 27 | type: 'module', 28 | } 29 | : { 30 | service_worker: './dist/background/index.mjs', 31 | }, 32 | icons: { 33 | 16: './assets/icon-16.png', 34 | 48: './assets/icon-48.png', 35 | 128: './assets/icon-512.png', 36 | }, 37 | permissions: [ 38 | '', 39 | 'contextMenus', 40 | ], 41 | host_permissions: ['*://*/*'], 42 | content_scripts: [ 43 | { 44 | matches: [ 45 | '', 46 | ], 47 | js: [ 48 | 'dist/contentScripts/index.global.js', 49 | ], 50 | }, 51 | ], 52 | web_accessible_resources: [ 53 | { 54 | resources: ['dist/contentScripts/style.css'], 55 | matches: [''], 56 | }, 57 | { 58 | resources: ['monaco-editor/*'], 59 | matches: [''], 60 | // use_dynamic_url: false, 61 | }, 62 | ], 63 | content_security_policy: { 64 | extension_pages: isDev 65 | // this is required on dev for Vite script to load 66 | ? `script-src \'self\' http://localhost:${port}; object-src \'self\'` 67 | : 'script-src \'self\'; object-src \'self\'', 68 | }, 69 | } 70 | 71 | if (isFirefox) { 72 | manifest.browser_specific_settings = { 73 | gecko: { 74 | id: 'xlogcustomcssdebugger@birdgg.me', 75 | }, 76 | } 77 | 78 | delete manifest.content_security_policy 79 | } 80 | 81 | // FIXME: not work in MV3 82 | // eslint-disable-next-line no-constant-condition 83 | if (isDev && false) { 84 | // for content script, as browsers will cache them for each reload, 85 | // we use a background script to always inject the latest version 86 | // see src/background/contentScriptHMR.ts 87 | delete manifest.content_scripts 88 | manifest.permissions?.push('webNavigation') 89 | } 90 | 91 | return manifest 92 | } 93 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { dirname, relative } from 'node:path' 4 | import type { UserConfig } from 'vite' 5 | import { defineConfig } from 'vite' 6 | import Vue from '@vitejs/plugin-vue' 7 | import Icons from 'unplugin-icons/vite' 8 | import IconsResolver from 'unplugin-icons/resolver' 9 | import Components from 'unplugin-vue-components/vite' 10 | import AutoImport from 'unplugin-auto-import/vite' 11 | import UnoCSS from 'unocss/vite' 12 | import { isDev, outputDir, port, r } from './scripts/utils' 13 | import packageJson from './package.json' 14 | 15 | export const sharedConfig: UserConfig = { 16 | root: r('src'), 17 | resolve: { 18 | alias: { 19 | '~/': `${r('src')}/`, 20 | }, 21 | }, 22 | define: { 23 | __DEV__: isDev, 24 | __NAME__: JSON.stringify(packageJson.name), 25 | }, 26 | plugins: [ 27 | Vue(), 28 | 29 | AutoImport({ 30 | imports: [ 31 | 'vue', 32 | { 33 | 'webextension-polyfill': [ 34 | ['*', 'browser'], 35 | ], 36 | }, 37 | ], 38 | dts: r('src/auto-imports.d.ts'), 39 | }), 40 | 41 | // https://github.com/antfu/unplugin-vue-components 42 | Components({ 43 | dirs: [r('src/components')], 44 | // generate `components.d.ts` for ts support with Volar 45 | dts: r('src/components.d.ts'), 46 | resolvers: [ 47 | // auto import icons 48 | IconsResolver({ 49 | componentPrefix: '', 50 | }), 51 | ], 52 | }), 53 | 54 | // https://github.com/antfu/unplugin-icons 55 | Icons(), 56 | 57 | // https://github.com/unocss/unocss 58 | UnoCSS(), 59 | 60 | // rewrite assets to use relative path 61 | { 62 | name: 'assets-rewrite', 63 | enforce: 'post', 64 | apply: 'build', 65 | transformIndexHtml(html, { path }) { 66 | return html.replace(/"\/assets\//g, `"${relative(dirname(path), '/assets')}/`) 67 | }, 68 | }, 69 | ], 70 | optimizeDeps: { 71 | include: [ 72 | 'vue', 73 | '@vueuse/core', 74 | 'webextension-polyfill', 75 | ], 76 | exclude: [ 77 | 'vue-demi', 78 | ], 79 | }, 80 | } 81 | 82 | export default defineConfig(({ command }) => ({ 83 | ...sharedConfig, 84 | base: command === 'serve' ? `http://localhost:${port}/` : '/dist/', 85 | server: { 86 | port, 87 | hmr: { 88 | host: 'localhost', 89 | }, 90 | }, 91 | build: { 92 | watch: isDev 93 | ? {} 94 | : undefined, 95 | outDir: r(`${outputDir}/dist`), 96 | emptyOutDir: false, 97 | sourcemap: isDev ? 'inline' : false, 98 | // https://developer.chrome.com/docs/webstore/program_policies/#:~:text=Code%20Readability%20Requirements 99 | terserOptions: { 100 | mangle: false, 101 | }, 102 | rollupOptions: { 103 | input: { 104 | options: r('src/options/index.html'), 105 | popup: r('src/popup/index.html'), 106 | }, 107 | }, 108 | }, 109 | test: { 110 | globals: true, 111 | environment: 'jsdom', 112 | }, 113 | })) 114 | -------------------------------------------------------------------------------- /src/contentScripts/views/CssEditor.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 |