From 546597bcc80efd36361874c012729f8c785eff45 Mon Sep 17 00:00:00 2001 From: arlo Date: Tue, 11 Aug 2026 14:27:50 +0800 Subject: [PATCH 1/3] refactor: webext --- package.json | 8 +- .../core/src/client/inject/runtime.test.ts | 61 ++- packages/core/src/client/inject/runtime.ts | 34 +- .../components/views/ViewIframe.vue | 98 ++--- .../core/src/client/webcomponents/index.ts | 2 + .../state/__tests__/context-cache.test.ts | 25 ++ .../state/__tests__/dock-groups.test.ts | 25 ++ .../src/client/webcomponents/state/context.ts | 12 +- .../webcomponents/state/dock-settings.ts | 13 + .../webcomponents/utils/iframe-url.test.ts | 39 ++ .../client/webcomponents/utils/iframe-url.ts | 26 ++ packages/core/src/node/server.test.ts | 92 ++++ packages/core/src/node/server.ts | 12 +- packages/core/src/node/ws.ts | 14 +- packages/kit/src/client/connection.test.ts | 27 ++ packages/kit/src/client/connection.ts | 21 +- packages/kit/src/client/index.ts | 8 +- packages/kit/src/client/remote.test.ts | 81 ---- packages/kit/src/client/remote.ts | 5 - .../webext/app/background/background.html | 9 + packages/webext/app/panel/App.vue | 99 +++++ packages/webext/app/panel/devtools-panel.html | 13 + .../webext/app/panel/inspected-window.test.ts | 53 +++ packages/webext/app/panel/inspected-window.ts | 21 + packages/webext/app/panel/main.ts | 15 + .../webext/app/panel/rpc-connection.test.ts | 56 +++ packages/webext/app/panel/rpc-connection.ts | 35 ++ packages/webext/app/panel/styles/main.css | 13 + packages/webext/app/scripts/background.ts | 0 .../webext/app/scripts/content-scripts.ts | 0 packages/webext/app/scripts/devtools-bg.ts | 58 +++ packages/webext/icons/128-gray.png | Bin 7612 -> 0 bytes packages/webext/icons/128-inactive.png | Bin 0 -> 12582 bytes packages/webext/icons/128.png | Bin 8210 -> 9618 bytes packages/webext/icons/16-gray.png | Bin 697 -> 0 bytes packages/webext/icons/16-inactive.png | Bin 0 -> 664 bytes packages/webext/icons/16.png | Bin 1335 -> 549 bytes packages/webext/icons/48-gray.png | Bin 2405 -> 0 bytes packages/webext/icons/48-inactive.png | Bin 0 -> 2450 bytes packages/webext/icons/48.png | Bin 2966 -> 2351 bytes packages/webext/manifest.json | 47 +- packages/webext/package.json | 19 +- .../webext/pages/devtools-background.html | 9 - packages/webext/pages/devtools-panel.html | 11 - packages/webext/popups/not-found.html | 5 - packages/webext/scripts/build-app.ts | 102 ----- packages/webext/scripts/zip.mjs | 118 +++++ packages/webext/src/background.ts | 1 - packages/webext/src/devtools-background.ts | 34 -- packages/webext/src/devtools-panel.ts | 59 --- packages/webext/src/event.ts | 4 - packages/webext/src/proxy.ts | 7 - packages/webext/src/window.ts | 11 - packages/webext/tsconfig.json | 10 +- packages/webext/tsdown.config.ts | 27 +- packages/webext/uno.config.ts | 42 ++ packages/webext/vite.config.ts | 35 ++ pnpm-lock.yaml | 403 ++++++++---------- pnpm-workspace.yaml | 1 + .../@vitejs/devtools-kit/client.snapshot.d.ts | 4 + .../@vitejs/devtools-kit/client.snapshot.js | 4 + .../client/webcomponents.snapshot.d.ts | 1 + .../devtools/client/webcomponents.snapshot.js | 1 + tsconfig.json | 3 +- turbo.json | 3 + 65 files changed, 1237 insertions(+), 699 deletions(-) create mode 100644 packages/core/src/client/webcomponents/utils/iframe-url.test.ts create mode 100644 packages/core/src/client/webcomponents/utils/iframe-url.ts create mode 100644 packages/core/src/node/server.test.ts delete mode 100644 packages/kit/src/client/remote.test.ts delete mode 100644 packages/kit/src/client/remote.ts create mode 100644 packages/webext/app/background/background.html create mode 100644 packages/webext/app/panel/App.vue create mode 100644 packages/webext/app/panel/devtools-panel.html create mode 100644 packages/webext/app/panel/inspected-window.test.ts create mode 100644 packages/webext/app/panel/inspected-window.ts create mode 100644 packages/webext/app/panel/main.ts create mode 100644 packages/webext/app/panel/rpc-connection.test.ts create mode 100644 packages/webext/app/panel/rpc-connection.ts create mode 100644 packages/webext/app/panel/styles/main.css create mode 100644 packages/webext/app/scripts/background.ts create mode 100644 packages/webext/app/scripts/content-scripts.ts create mode 100644 packages/webext/app/scripts/devtools-bg.ts delete mode 100644 packages/webext/icons/128-gray.png create mode 100644 packages/webext/icons/128-inactive.png delete mode 100644 packages/webext/icons/16-gray.png create mode 100644 packages/webext/icons/16-inactive.png delete mode 100644 packages/webext/icons/48-gray.png create mode 100644 packages/webext/icons/48-inactive.png delete mode 100644 packages/webext/pages/devtools-background.html delete mode 100644 packages/webext/pages/devtools-panel.html delete mode 100644 packages/webext/popups/not-found.html delete mode 100644 packages/webext/scripts/build-app.ts create mode 100644 packages/webext/scripts/zip.mjs delete mode 100644 packages/webext/src/background.ts delete mode 100644 packages/webext/src/devtools-background.ts delete mode 100644 packages/webext/src/devtools-panel.ts delete mode 100644 packages/webext/src/event.ts delete mode 100644 packages/webext/src/proxy.ts delete mode 100644 packages/webext/src/window.ts create mode 100644 packages/webext/uno.config.ts create mode 100644 packages/webext/vite.config.ts diff --git a/package.json b/package.json index 08e6c4f2b..9ec0c8084 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "test:e2e": "pnpm -C e2e run test", "test": "vitest", "typecheck": "vue-tsc -b", - "watch": "turbo watch build" + "watch": "turbo watch build", + "zip": "pnpm run zip:webext", + "zip:webext": "turbo run build --filter=@vitejs/devtools-webext && node packages/webext/scripts/zip.mjs" }, "scripts-info": { "actionspack": "Update pinned GitHub Actions versions", @@ -53,7 +55,9 @@ "test:e2e": "Run end-to-end tests", "test": "Run unit tests with Vitest", "typecheck": "Type-check all packages with vue-tsc", - "watch": "Rebuild changed packages on file change via turbo watch" + "watch": "Rebuild changed packages on file change via turbo watch", + "zip": "Build and package the browser extension", + "zip:webext": "Build and package the browser extension as a zip file" }, "devDependencies": { "@antfu/eslint-config": "catalog:devtools", diff --git a/packages/core/src/client/inject/runtime.test.ts b/packages/core/src/client/inject/runtime.test.ts index d653aaf5e..4f54398a6 100644 --- a/packages/core/src/client/inject/runtime.test.ts +++ b/packages/core/src/client/inject/runtime.test.ts @@ -1,9 +1,13 @@ +import type { DevframeConnection } from 'devframe/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { startDevTools } from './runtime' +const DEVFRAME_CONNECTION_KEY = '__DEVFRAME_CONNECTION__' +const DEVFRAME_CONNECTION_META_KEY = '__DEVFRAME_CONNECTION_META__' + const mocks = vi.hoisted(() => ({ getDevToolsRpcClient: vi.fn( - (_options: { baseURL: string[] }) => new Promise(() => {}), + (_options: { connection: DevframeConnection }) => new Promise(() => {}), ), })) @@ -24,30 +28,67 @@ describe('injected DevTools runtime', () => { beforeEach(() => { const hostWindow = { addEventListener: vi.fn(), + location: { + href: 'http://localhost:5173/', + }, } as unknown as Window Object.defineProperty(hostWindow, 'parent', { value: hostWindow }) vi.stubGlobal('window', hostWindow) - - startDevTools('normal') + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + ok: true, + url: 'http://localhost:5173/__devtools/__connection.json', + json: () => Promise.resolve({ + backend: 'websocket', + websocket: { path: '__ws' }, + }), + })) }) afterEach(() => { + delete (globalThis as Record)[DEVFRAME_CONNECTION_KEY] + delete (globalThis as Record)[DEVFRAME_CONNECTION_META_KEY] vi.unstubAllGlobals() mocks.getDevToolsRpcClient.mockClear() }) - it('preserves the host-relative mount path as the primary base', () => { + it('preserves the host-relative mount path as the primary base', async () => { + startDevTools('normal') + await vi.waitFor(() => expect(mocks.getDevToolsRpcClient).toHaveBeenCalled()) const options = mocks.getDevToolsRpcClient.mock.calls[0]![0] - expect(options.baseURL).toHaveLength(2) - expect(options.baseURL[0]).toBe('/__devtools/') + expect(options.connection.metaBaseUrl).toBe( + 'http://localhost:5173/__devtools/__connection.json', + ) }) - it('adds the Vite module origin as the fallback base', () => { + it('passes the prepared connection to the RPC client', async () => { + startDevTools('normal') + await vi.waitFor(() => expect(mocks.getDevToolsRpcClient).toHaveBeenCalled()) const options = mocks.getDevToolsRpcClient.mock.calls[0]![0] - expect(options.baseURL[1]).toBe( - new URL('/__devtools/', import.meta.url).href, - ) + expect(options.connection.connectionMeta).toEqual({ + backend: 'websocket', + websocket: { path: '__ws' }, + }) }) + + it.each(['normal', 'passive', 'hidden'] as const)( + 'publishes a standard connection without unnecessarily mounting the %s overlay', + async (mode) => { + startDevTools(mode) + + await vi.waitFor(() => { + expect((globalThis as Record)[DEVFRAME_CONNECTION_KEY]).toEqual({ + connectionMeta: { + backend: 'websocket', + websocket: { path: '__ws' }, + }, + metaBaseUrl: 'http://localhost:5173/__devtools/__connection.json', + authToken: undefined, + }) + }) + if (mode !== 'normal') + expect(mocks.getDevToolsRpcClient).not.toHaveBeenCalled() + }, + ) }) diff --git a/packages/core/src/client/inject/runtime.ts b/packages/core/src/client/inject/runtime.ts index 0a5d2c520..2d4de62ac 100644 --- a/packages/core/src/client/inject/runtime.ts +++ b/packages/core/src/client/inject/runtime.ts @@ -5,6 +5,7 @@ import type { DockPanelStorage } from '@vitejs/devtools-kit/client' import { CLIENT_CONTEXT_KEY, getDevToolsRpcClient } from '@vitejs/devtools-kit/client' import { DEVTOOLS_MOUNT_PATH } from '@vitejs/devtools-kit/constants' import { useLocalStorage } from '@vueuse/core' +import { setupDevframeConnection } from 'devframe/client' import { DEVTOOLS_HIDE_EVENT, DEVTOOLS_MODE_FILENAME } from '../../constants' import { createDocksContext } from '../webcomponents/state/context' @@ -46,18 +47,24 @@ async function persistNormalMode(enabled: boolean): Promise { } } +function setupConnection() { + return setupDevframeConnection({ + // Prefer the host page's origin. When the page is served by another + // backend, fall back to the Vite origin that loaded this module. + baseURL: [ + DEVTOOLS_MOUNT_PATH, + new URL(DEVTOOLS_MOUNT_PATH, import.meta.url).href, + ], + }) +} + async function mountDock(): Promise { if (dockEl) return - // Prefer the host page's origin for backwards compatibility. When the page - // is served by another backend, fall back to the Vite origin that loaded - // this module. + const connection = await setupConnection() const rpc = await getDevToolsRpcClient({ - baseURL: [ - DEVTOOLS_MOUNT_PATH, - new URL(DEVTOOLS_MOUNT_PATH, import.meta.url).href, - ], + connection, }) const state = useLocalStorage( @@ -168,6 +175,8 @@ export function startDevTools(initialMode: InjectMode): void { // eslint-disable-next-line no-console console.log('[VITE DEVTOOLS] Client injected') + const connectionReady = setupConnection() + // Only passive mode remembers the reveal across loads; hidden is per-session. const persist = initialMode === 'passive' @@ -175,8 +184,13 @@ export function startDevTools(initialMode: InjectMode): void { void deactivate(persist) }) - if (initialMode === 'normal') - void mountDock() - else + if (initialMode === 'normal') { + void connectionReady.then(() => mountDock()) + } + else { + // Passive/hidden mode prepares the external-viewer connection eagerly, + // while keeping a failed attempt retryable when the overlay is activated. + void connectionReady.catch(() => {}) armPassive(persist) + } } diff --git a/packages/core/src/client/webcomponents/components/views/ViewIframe.vue b/packages/core/src/client/webcomponents/components/views/ViewIframe.vue index cffe6c228..1056c0149 100644 --- a/packages/core/src/client/webcomponents/components/views/ViewIframe.vue +++ b/packages/core/src/client/webcomponents/components/views/ViewIframe.vue @@ -3,10 +3,11 @@ import type { DevToolsViewIframe } from '@vitejs/devtools-kit' import type { DocksContext } from '@vitejs/devtools-kit/client' import type { IframePanes } from 'iframe-pane' import type { CSSProperties } from 'vue' -import { REMOTE_CONNECTION_KEY } from '@vitejs/devtools-kit/constants' -import { computed, nextTick, onMounted, onUnmounted, ref, useTemplateRef, watchEffect } from 'vue' +import { buildRemoteDevframeUrl, resolveDockUrl, stripRemoteConnectionFromUrl } from '@vitejs/devtools-kit/client' +import { computed, nextTick, onMounted, onUnmounted, ref, useTemplateRef, watch, watchEffect } from 'vue' import { getEntryGroup } from '../../state/dock-settings' import { sharedStateToRef } from '../../state/docks' +import { getDocksContextOrigin, getWindowOrigin } from '../../utils/iframe-url' const props = defineProps<{ context: DocksContext @@ -15,34 +16,6 @@ const props = defineProps<{ iframeStyle?: CSSProperties }>() -function stripRemoteConnectionParam(url: string): string { - // Remove the remote connection descriptor so the auth token isn't exposed - // in the address bar (user could accidentally copy it). - let result = url - - const hashIdx = result.indexOf('#') - if (hashIdx !== -1) { - const hash = result.slice(hashIdx + 1) - const filtered = hash - .split('&') - .filter(part => !part.startsWith(`${REMOTE_CONNECTION_KEY}=`)) - .join('&') - result = filtered ? `${result.slice(0, hashIdx)}#${filtered}` : result.slice(0, hashIdx) - } - - const qIdx = result.indexOf('?') - if (qIdx !== -1) { - const query = result.slice(qIdx + 1) - const filtered = query - .split('&') - .filter(part => !part.startsWith(`${REMOTE_CONNECTION_KEY}=`)) - .join('&') - result = filtered ? `${result.slice(0, qIdx)}?${filtered}` : result.slice(0, qIdx) - } - - return result -} - const settings = sharedStateToRef(props.context.docks.settings) const showAddressBar = computed(() => settings.value.showIframeAddressBar ?? true) const isEdgeMode = computed(() => props.context.panel.store.mode === 'edge') @@ -61,8 +34,23 @@ const viewFrame = useTemplateRef('viewFrame') const urlInputRef = useTemplateRef('urlInput') // Address bar state -const currentUrl = ref(props.entry.url) -const editingUrl = ref(props.entry.url) +const currentPageOrigin = computed(() => getWindowOrigin()) +const resolvedBaseOrigin = computed(() => getDocksContextOrigin(props.context)) +const resolvedEntryUrl = computed(() => { + const url = resolveDockUrl(props.entry.url, props.context.rpc.connection) + if (currentPageOrigin.value === resolvedBaseOrigin.value) + return url + + try { + if (new URL(url).origin === resolvedBaseOrigin.value) + return buildRemoteDevframeUrl(url, props.context.rpc.connection) + } + catch {} + + return url +}) +const currentUrl = ref(resolvedEntryUrl.value) +const editingUrl = ref(resolvedEntryUrl.value) const isEditing = ref(false) // Shared-iframe soft navigation: an anchor iframe dock and each of its member @@ -77,16 +65,6 @@ const iframeElement = computed(() => { return props.panes.get(paneKey.value)?.iframe }) -// Get current page's origin for comparison -const currentPageOrigin = computed(() => { - try { - return window.location.origin - } - catch { - return '' - } -}) - // Check if iframe URL is cross-origin const isCrossOrigin = computed(() => { try { @@ -100,7 +78,7 @@ const isCrossOrigin = computed(() => { // Display URL - hides host if same as current page const displayUrl = computed(() => { - const sanitized = stripRemoteConnectionParam(currentUrl.value) + const sanitized = stripRemoteConnectionFromUrl(currentUrl.value) if (isCrossOrigin.value) { return sanitized } @@ -132,17 +110,9 @@ function navigateTo(url: string) { if (!iframe) return - // Ensure URL has protocol - let normalizedUrl = url.trim() - if (normalizedUrl && !/^https?:\/\//i.test(normalizedUrl)) { - // If it starts with /, treat as same-origin path - if (normalizedUrl.startsWith('/')) { - normalizedUrl = `${window.location.origin}${normalizedUrl}` - } - else { - normalizedUrl = `http://${normalizedUrl}` - } - } + const normalizedUrl = resolveDockUrl(url, props.context.rpc.connection) + if (!normalizedUrl) + return currentUrl.value = normalizedUrl editingUrl.value = normalizedUrl @@ -208,7 +178,7 @@ onMounted(() => { // a shared frame this is also the boot deep-link: the first member (or the // anchor) to become visible seeds the src, and every later switch soft-navs. const pane = props.panes.ensure(paneKey.value, { - src: props.entry.url, + src: resolvedEntryUrl.value, style: { boxShadow: 'none', outline: 'none' }, }) const iframe = pane.iframe @@ -253,6 +223,24 @@ onMounted(() => { }) }) +watch( + resolvedEntryUrl, + (newUrl, oldUrl) => { + if (!newUrl || newUrl === oldUrl) + return + + if (currentUrl.value !== oldUrl) + return + + currentUrl.value = newUrl + editingUrl.value = newUrl + + const iframe = iframeElement.value + if (iframe && iframe.src !== newUrl) + iframe.src = newUrl + }, +) + onUnmounted(() => { const pane = props.panes.get(paneKey.value) if (pane && onIframeLoad) diff --git a/packages/core/src/client/webcomponents/index.ts b/packages/core/src/client/webcomponents/index.ts index c7c0ba582..4797f23ce 100644 --- a/packages/core/src/client/webcomponents/index.ts +++ b/packages/core/src/client/webcomponents/index.ts @@ -1,5 +1,6 @@ export * from './components/DockEmbedded' export * from './components/DockStandalone' + /** * Opt-in strict types for the json-render base catalog — most spec authors * use the fully open `JsonRenderElement` from `@vitejs/devtools-kit` instead; @@ -29,4 +30,5 @@ export type { TreeProps, } from './json-render/registry' +export * from './state/context' export * from './state/docks' diff --git a/packages/core/src/client/webcomponents/state/__tests__/context-cache.test.ts b/packages/core/src/client/webcomponents/state/__tests__/context-cache.test.ts index a1f44e805..ec95f2d0f 100644 --- a/packages/core/src/client/webcomponents/state/__tests__/context-cache.test.ts +++ b/packages/core/src/client/webcomponents/state/__tests__/context-cache.test.ts @@ -23,6 +23,16 @@ function createMockRpc(entries: DevToolsDockEntry[] = []): DevToolsRpcClient { }) return { + connection: { + connectionMeta: { + backend: 'websocket', + }, + metaBaseUrl: 'http://localhost:5173/__devtools/__connection.json', + }, + connectionMeta: { + backend: 'websocket', + baseUrl: 'http://localhost:5173/__devtools/__connection.json', + }, client: { register: () => () => {}, }, @@ -64,4 +74,19 @@ describe('dock state caches', () => { expect(entriesA1).toBe(entriesA2) expect(entriesA1).not.toBe(entriesB) }) + + it('resolves server-hosted dock icons against the RPC server', async () => { + const rpc = createMockRpc([{ + id: 'vite', + type: 'iframe', + title: 'Vite', + icon: '/__devtools-vite/favicon.svg', + url: '/__devtools-vite/', + }]) + + const context = await createDocksContext('standalone', rpc) + + expect(context.docks.entries[0]?.icon) + .toBe('http://localhost:5173/__devtools-vite/favicon.svg') + }) }) diff --git a/packages/core/src/client/webcomponents/state/__tests__/dock-groups.test.ts b/packages/core/src/client/webcomponents/state/__tests__/dock-groups.test.ts index 96b93c7e1..27e8e7d02 100644 --- a/packages/core/src/client/webcomponents/state/__tests__/dock-groups.test.ts +++ b/packages/core/src/client/webcomponents/state/__tests__/dock-groups.test.ts @@ -66,6 +66,31 @@ describe('dock groups', () => { expect(ids).not.toContain('nuxt:pages') }) + it('drops an empty group before creating its category bucket', () => { + const grouped = docksGroupByCategories([ + group('viteplus', { category: 'framework' }), + iframe('unocss', { category: 'web' }), + ], settings, { collapseGroups: true }) + + expect(grouped.map(([category]) => category)).toEqual(['web']) + expect(grouped.flatMap(([, items]) => items.map(i => i.id))).toEqual(['unocss']) + }) + + it('keeps an empty-looking group whose default child remains reachable', () => { + const grouped = docksGroupByCategories([ + group('viteplus', { + category: 'framework', + defaultChildId: 'vite', + } as Partial), + iframe('vite', { + groupId: 'viteplus', + visibility: 'false', + } as Partial), + ], settings, { collapseGroups: true }) + + expect(grouped.flatMap(([, items]) => items.map(i => i.id))).toEqual(['viteplus']) + }) + it('keeps members visible when not collapsing', () => { const grouped = docksGroupByCategories(entries, settings) const ids = grouped.flatMap(([, items]) => items.map(i => i.id)) diff --git a/packages/core/src/client/webcomponents/state/context.ts b/packages/core/src/client/webcomponents/state/context.ts index 52060b2c9..0e8cfd915 100644 --- a/packages/core/src/client/webcomponents/state/context.ts +++ b/packages/core/src/client/webcomponents/state/context.ts @@ -4,7 +4,7 @@ import type { SharedState } from 'devframe/utils/shared-state' import type { WhenContext } from 'devframe/utils/when' import type { Ref } from 'vue' import type { DevToolsDocksUserSettings } from './dock-settings' -import { attachDevToolsFrameNav } from '@vitejs/devtools-kit/client' +import { attachDevToolsFrameNav, resolveDockIcon } from '@vitejs/devtools-kit/client' import { DEFAULT_STATE_USER_SETTINGS, DEVTOOLS_MOUNT_PATH } from '@vitejs/devtools-kit/constants' import { computed, markRaw, reactive, ref, toRefs, watch, watchEffect } from 'vue' import { DEVTOOLS_HIDE_EVENT, DEVTOOLS_MODE_FILENAME } from '../../../constants' @@ -28,6 +28,10 @@ export async function createDocksContext( } const dockEntries = await useDocksEntries(rpc) + const resolveEntryIcon = (entry: T): T => ({ + ...entry, + icon: resolveDockIcon(entry.icon, rpc.connection), + }) // Client-only dock registry (0.7.10 `DocksEntriesContext` API). Docks // registered here live in this page only, merged over the server-provided @@ -35,7 +39,7 @@ export async function createDocksContext( // merge semantics of `@devframes/hub`'s own client host. const clientDocks = reactive(new Map()) const entries = computed(() => { - const server = dockEntries.value + const server = dockEntries.value.map(resolveEntryIcon) if (clientDocks.size === 0) return server const seen = new Set() @@ -43,11 +47,11 @@ export async function createDocksContext( for (const entry of server) { seen.add(entry.id) // a client dock sharing a server id overrides it in the local merge - merged.push(clientDocks.get(entry.id) ?? entry) + merged.push(resolveEntryIcon(clientDocks.get(entry.id) ?? entry)) } for (const [id, entry] of clientDocks) { if (!seen.has(id)) - merged.push(entry) + merged.push(resolveEntryIcon(entry)) } return merged }) diff --git a/packages/core/src/client/webcomponents/state/dock-settings.ts b/packages/core/src/client/webcomponents/state/dock-settings.ts index 5e5c54996..fcac4bb5e 100644 --- a/packages/core/src/client/webcomponents/state/dock-settings.ts +++ b/packages/core/src/client/webcomponents/state/dock-settings.ts @@ -300,6 +300,19 @@ export function docksGroupByCategories( if (!includeHidden && docksHidden.includes(entry.id)) continue + // An empty group has no button to render on the collapsed dock bar. Drop + // it before creating its category bucket so category-only UI such as + // separators cannot survive after the group itself disappears. + if ( + collapseGroups + && !includeHidden + && entry.type === 'group' + && getGroupMembers(entries, entry.id, settings, { whenContext }).length === 0 + && !resolveGroupDefaultChild(entries, entry.id, entry.defaultChildId, whenContext) + ) { + continue + } + // Outer bucket: the group's category for grouped members, else the entry's // own category. Orphans (groupId with no registered group) fall through to // their own `category`. diff --git a/packages/core/src/client/webcomponents/utils/iframe-url.test.ts b/packages/core/src/client/webcomponents/utils/iframe-url.test.ts new file mode 100644 index 000000000..9555e810d --- /dev/null +++ b/packages/core/src/client/webcomponents/utils/iframe-url.test.ts @@ -0,0 +1,39 @@ +import type { DocksContext } from '@vitejs/devtools-kit/client' +import { describe, expect, it } from 'vitest' +import { getDocksContextOrigin, getRpcConnectionOrigin } from './iframe-url' + +function createContext(metaBaseUrl?: string, legacyBaseUrl?: string): DocksContext { + const connectionMeta = { + backend: 'websocket' as const, + baseUrl: legacyBaseUrl, + } + return { + rpc: { + connection: metaBaseUrl + ? { + connectionMeta, + metaBaseUrl, + } + : undefined, + connectionMeta, + }, + } as DocksContext +} + +describe('iframe URL resolution', () => { + it('uses the Devframe connection origin for standalone viewers', () => { + const context = createContext('http://localhost:5173/__devtools/__connection.json') + + expect(getDocksContextOrigin(context)).toBe('http://localhost:5173') + expect(getRpcConnectionOrigin(context.rpc)).toBe('http://localhost:5173') + }) + + it('supports legacy metadata carrying its source URL', () => { + const context = createContext( + undefined, + 'http://localhost:4173/__devtools/__connection.json', + ) + + expect(getDocksContextOrigin(context)).toBe('http://localhost:4173') + }) +}) diff --git a/packages/core/src/client/webcomponents/utils/iframe-url.ts b/packages/core/src/client/webcomponents/utils/iframe-url.ts new file mode 100644 index 000000000..4b68cc9af --- /dev/null +++ b/packages/core/src/client/webcomponents/utils/iframe-url.ts @@ -0,0 +1,26 @@ +import type { DevToolsRpcClient, DocksContext } from '@vitejs/devtools-kit/client' + +export function getWindowOrigin(): string { + try { + return window.location.origin + } + catch { + return '' + } +} + +export function getRpcConnectionOrigin(rpc: DevToolsRpcClient): string { + const baseUrl = rpc.connection?.metaBaseUrl + ?? rpc.connectionMeta?.baseUrl + if (baseUrl) { + try { + return new URL(baseUrl).origin + } + catch {} + } + return getWindowOrigin() +} + +export function getDocksContextOrigin(context: DocksContext): string { + return getRpcConnectionOrigin(context.rpc) +} diff --git a/packages/core/src/node/server.test.ts b/packages/core/src/node/server.test.ts new file mode 100644 index 000000000..3a1b32f74 --- /dev/null +++ b/packages/core/src/node/server.test.ts @@ -0,0 +1,92 @@ +import type { CreateWsServerOptions } from './ws' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createDevToolsMiddleware } from './server' + +const mocks = vi.hoisted(() => ({ + getConnectionMeta: vi.fn(async () => ({ + backend: 'websocket', + websocket: { path: '__ws' }, + })), + mountStaticHandler: vi.fn(), + registerFromUrl: vi.fn<(url: string) => string | undefined>(), + isAllowed: vi.fn<(origin: string | undefined) => boolean>(() => false), +})) + +vi.mock('devframe/utils/serve-static', () => ({ + mountStaticHandler: mocks.mountStaticHandler, +})) + +vi.mock('./ws', () => ({ + createWsServer: vi.fn(async () => ({ + rpc: {}, + getConnectionMeta: mocks.getConnectionMeta, + viewerOrigins: { + registerFromUrl: mocks.registerFromUrl, + isAllowed: mocks.isAllowed, + }, + })), +})) + +function createOptions(): CreateWsServerOptions { + return { + cwd: process.cwd(), + websocket: { + host: 'localhost', + }, + context: { + host: { + provideConnectionMeta: vi.fn(), + }, + }, + } as unknown as CreateWsServerOptions +} + +beforeEach(() => { + vi.clearAllMocks() + mocks.registerFromUrl.mockReturnValue(undefined) + mocks.isAllowed.mockReturnValue(false) +}) + +describe('connection metadata endpoint', () => { + it('allows a registered requesting browser extension origin', async () => { + const { h3 } = await createDevToolsMiddleware(createOptions()) + const origin = 'chrome-extension://abcdefghijklmnopabcdefghijklmnop' + mocks.registerFromUrl.mockReturnValue(origin) + mocks.isAllowed.mockReturnValue(true) + + const response = await h3.request('/__connection.json?devframe_viewer_origin=extension&devframe_viewer_origin_token=token', { + headers: { + origin, + }, + }) + + expect(response.status).toBe(200) + expect(response.headers.get('access-control-allow-origin')).toBe(origin) + expect(response.headers.get('cache-control')).toBe('no-store') + expect(mocks.registerFromUrl).toHaveBeenCalledWith('http://localhost/__connection.json?devframe_viewer_origin=extension&devframe_viewer_origin_token=token') + }) + + it('registers an explicitly supplied panel origin when extension fetch omits Origin', async () => { + const { h3 } = await createDevToolsMiddleware(createOptions()) + const origin = 'chrome-extension://abcdefghijklmnopabcdefghijklmnop' + mocks.registerFromUrl.mockReturnValue(origin) + + const response = await h3.request(`/__connection.json?devframe_viewer_origin=${encodeURIComponent(origin)}&devframe_viewer_origin_token=token`) + + expect(response.status).toBe(200) + expect(response.headers.get('access-control-allow-origin')).toBe(origin) + }) + + it('does not add CORS headers for ordinary web origins', async () => { + const { h3 } = await createDevToolsMiddleware(createOptions()) + + const response = await h3.request('/__connection.json', { + headers: { + origin: 'https://example.com', + }, + }) + + expect(response.status).toBe(200) + expect(response.headers.has('access-control-allow-origin')).toBe(false) + }) +}) diff --git a/packages/core/src/node/server.ts b/packages/core/src/node/server.ts index 71701d299..fdf2ad1d3 100644 --- a/packages/core/src/node/server.ts +++ b/packages/core/src/node/server.ts @@ -19,7 +19,7 @@ export interface DevToolsMiddleware { export async function createDevToolsMiddleware(options: CreateWsServerOptions): Promise { const h3 = new H3() - const { rpc, getConnectionMeta } = await createWsServer(options) + const { rpc, getConnectionMeta, viewerOrigins } = await createWsServer(options) // Hand the host the live connection-meta getter so each mounted devframe's // `mountConnectionMeta` middleware can serve it at the devframe's own base @@ -28,6 +28,16 @@ export async function createDevToolsMiddleware(options: CreateWsServerOptions): ;(options.context.host as ViteDevToolsHost).provideConnectionMeta?.(getConnectionMeta) h3.use(`/${DEVTOOLS_CONNECTION_META_FILENAME}`, defineHandler(async (event) => { + const requestOrigin = event.req.headers.get('origin') ?? undefined + const registeredOrigin = viewerOrigins.registerFromUrl(event.req.url) + const responseOrigin = requestOrigin && viewerOrigins.isAllowed(requestOrigin) + ? requestOrigin + : registeredOrigin + if (responseOrigin) { + event.res.headers.set('Access-Control-Allow-Origin', responseOrigin) + event.res.headers.set('Vary', 'Origin') + } + event.res.headers.set('Cache-Control', 'no-store') event.res.headers.set('Content-Type', 'application/json') return JSON.stringify(await getConnectionMeta()) })) diff --git a/packages/core/src/node/ws.ts b/packages/core/src/node/ws.ts index da5ffaf4d..9c61870c4 100644 --- a/packages/core/src/node/ws.ts +++ b/packages/core/src/node/ws.ts @@ -12,7 +12,7 @@ import { DEVTOOLS_WS_PATH, DEVTOOLS_WS_ROUTE } from '@vitejs/devtools-kit/consta import { isAnonymousRpcMethod } from 'devframe/constants' import { getInternalContext } from 'devframe/node/hub-internals' import { createRpcServer } from 'devframe/rpc/server' -import { attachWsRpcTransport } from 'devframe/rpc/transports/ws-server' +import { attachWsRpcTransport, createWsOriginRegistry } from 'devframe/rpc/transports/ws-server' import { colors as c } from 'devframe/utils/colors' import { getPort } from 'get-port-please' import { createDebug } from 'obug' @@ -152,11 +152,17 @@ export async function createWsServer(options: CreateWsServerOptions) { // added here isn't visible through `context.viteConfig.devtools.config` until Vite re-vendors // it — the same gap `vite-augment.ts` works around for `Plugin.devtools`. Read it through a // narrow cast rather than waiting on that. - const allowedOrigins = (context.viteConfig.devtools?.config as DevToolsConfig | undefined)?.allowedOrigins + const viewerOrigins = createWsOriginRegistry({ + allowedOrigins: (context.viteConfig.devtools?.config as DevToolsConfig | undefined)?.allowedOrigins, + validateOrigin(origin) { + const protocol = new URL(origin).protocol + return protocol === 'chrome-extension:' || protocol === 'moz-extension:' + }, + }) attachWsRpcTransport(rpcGroup, { ...binding, - allowedOrigins, + allowedOrigins: viewerOrigins, definitions: rpcHost.definitions, onConnected: (peer, meta) => { // crossws exposes the upgrade request (with its query string + headers) @@ -211,6 +217,7 @@ export async function createWsServer(options: CreateWsServerOptions) { // client (proxy-safe); the dedicated-port form is a bare port number. websocket: routeBound ? { path: DEVTOOLS_WS_ROUTE } : port!, jsonSerializableMethods, + viewerOriginToken: viewerOrigins.token, } } @@ -219,5 +226,6 @@ export async function createWsServer(options: CreateWsServerOptions) { rpc: rpcGroup, rpcHost, getConnectionMeta, + viewerOrigins, } } diff --git a/packages/kit/src/client/connection.test.ts b/packages/kit/src/client/connection.test.ts index 78d3ada7a..45721ee8f 100644 --- a/packages/kit/src/client/connection.test.ts +++ b/packages/kit/src/client/connection.test.ts @@ -3,15 +3,18 @@ import { getDevToolsRpcClient } from './connection' const mocks = vi.hoisted(() => ({ getDevframeRpcClient: vi.fn(async () => ({})), + parseRemoteConnection: vi.fn(), })) vi.mock('@devframes/hub/client', () => ({ getDevframeRpcClient: mocks.getDevframeRpcClient, + parseRemoteConnection: mocks.parseRemoteConnection, })) describe('getDevToolsRpcClient', () => { beforeEach(() => { mocks.getDevframeRpcClient.mockClear() + mocks.parseRemoteConnection.mockReset() }) it('disables devframe simple auth in favor of the Vite DevTools auth UI', async () => { @@ -30,4 +33,28 @@ describe('getDevToolsRpcClient', () => { simpleAuth: false, }) }) + + it('uses a remote connection descriptor in external viewers', async () => { + mocks.parseRemoteConnection.mockReturnValue({ + v: 1, + backend: 'websocket', + websocket: 'ws://localhost:5173/__devtools/__ws', + authToken: 'trusted-token', + origin: 'http://localhost:5173', + }) + + await getDevToolsRpcClient() + + expect(mocks.getDevframeRpcClient).toHaveBeenCalledWith({ + connection: { + connectionMeta: { + backend: 'websocket', + websocket: 'ws://localhost:5173/__devtools/__ws', + }, + metaBaseUrl: 'http://localhost:5173', + authToken: 'trusted-token', + }, + simpleAuth: false, + }) + }) }) diff --git a/packages/kit/src/client/connection.ts b/packages/kit/src/client/connection.ts index 85de75012..34457174e 100644 --- a/packages/kit/src/client/connection.ts +++ b/packages/kit/src/client/connection.ts @@ -1,5 +1,5 @@ import type { DevframeRpcClient, DevframeRpcClientOptions } from '@devframes/hub/client' -import { getDevframeRpcClient } from '@devframes/hub/client' +import { getDevframeRpcClient, parseRemoteConnection } from '@devframes/hub/client' /** * The Vite DevTools flavour of devframe's {@link getDevframeRpcClient}. Kept as @@ -19,6 +19,25 @@ import { getDevframeRpcClient } from '@devframes/hub/client' export function getDevToolsRpcClient( options: DevframeRpcClientOptions = {}, ): Promise { + const remote = options.connection ? null : parseRemoteConnection() + if (remote) { + const { + authToken, + origin, + v: _version, + ...connectionMeta + } = remote + return getDevframeRpcClient({ + ...options, + connection: { + connectionMeta, + metaBaseUrl: origin, + authToken, + }, + simpleAuth: false, + }) + } + return getDevframeRpcClient({ ...options, simpleAuth: false, diff --git a/packages/kit/src/client/index.ts b/packages/kit/src/client/index.ts index 420af4857..782fc4d61 100644 --- a/packages/kit/src/client/index.ts +++ b/packages/kit/src/client/index.ts @@ -3,9 +3,11 @@ export * from './connection' export * from './context' export * from './docks' export * from './frame-nav' -export * from './remote' export { + buildRemoteDevframeUrl, + connectRemoteDevframe as connectRemoteDevTools, + type ConnectRemoteDevframeOptions as ConnectRemoteDevToolsOptions, type DevframeClientRpcHost as DevToolsClientRpcHost, type DevframeRpcClient as DevToolsRpcClient, type DevframeRpcClientCall as DevToolsRpcClientCall, @@ -14,6 +16,10 @@ export { type DevframeRpcClientMode as DevToolsRpcClientMode, type DevframeRpcClientOptions as DevToolsRpcClientOptions, type DevframeRpcContext as DevToolsRpcContext, + parseRemoteConnection, + resolveDockIcon, + resolveDockUrl, type RpcStreamingClientHost, type StreamingSubscribeOptions, + stripRemoteConnectionFromUrl, } from '@devframes/hub/client' diff --git a/packages/kit/src/client/remote.test.ts b/packages/kit/src/client/remote.test.ts deleted file mode 100644 index 09ccc0423..000000000 --- a/packages/kit/src/client/remote.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { RemoteConnectionInfo } from '../types' -import { Buffer } from 'node:buffer' -import { REMOTE_CONNECTION_KEY } from 'devframe/constants' -import { describe, expect, it } from 'vitest' -import { parseRemoteConnection } from './remote' - -function encode(payload: Partial): string { - return Buffer.from(JSON.stringify(payload), 'utf8') - .toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, '') -} - -const VALID_PAYLOAD: RemoteConnectionInfo = { - v: 1, - backend: 'websocket', - websocket: 'ws://localhost:7812', - authToken: 'happy-lion-7', - origin: 'http://localhost:5173', -} - -describe('parseRemoteConnection', () => { - it('returns null when the URL carries no descriptor', () => { - expect(parseRemoteConnection('https://example.com/page')).toBeNull() - }) - - it('parses a fragment-transport descriptor', () => { - const encoded = encode(VALID_PAYLOAD) - const url = `https://example.com/page#${REMOTE_CONNECTION_KEY}=${encoded}` - expect(parseRemoteConnection(url)).toEqual(VALID_PAYLOAD) - }) - - it('parses a query-transport descriptor', () => { - const encoded = encode(VALID_PAYLOAD) - const url = `https://example.com/page?${REMOTE_CONNECTION_KEY}=${encoded}` - expect(parseRemoteConnection(url)).toEqual(VALID_PAYLOAD) - }) - - it('prefers fragment over query when both are present', () => { - const fragPayload = { ...VALID_PAYLOAD, authToken: 'from-fragment' } - const queryPayload = { ...VALID_PAYLOAD, authToken: 'from-query' } - const url = `https://example.com/page` - + `?${REMOTE_CONNECTION_KEY}=${encode(queryPayload)}` - + `#${REMOTE_CONNECTION_KEY}=${encode(fragPayload)}` - expect(parseRemoteConnection(url)?.authToken).toBe('from-fragment') - }) - - it('accepts a raw fragment string', () => { - const encoded = encode(VALID_PAYLOAD) - expect(parseRemoteConnection(`#${REMOTE_CONNECTION_KEY}=${encoded}`)).toEqual(VALID_PAYLOAD) - }) - - it('accepts a raw query string', () => { - const encoded = encode(VALID_PAYLOAD) - expect(parseRemoteConnection(`?${REMOTE_CONNECTION_KEY}=${encoded}`)).toEqual(VALID_PAYLOAD) - }) - - it('throws on malformed base64', () => { - const url = `https://example.com/page#${REMOTE_CONNECTION_KEY}=not@valid!base64` - expect(() => parseRemoteConnection(url)).toThrow(/Failed to decode/) - }) - - it('throws on version mismatch', () => { - const encoded = encode({ ...VALID_PAYLOAD, v: 2 as 1 }) - const url = `https://example.com/page#${REMOTE_CONNECTION_KEY}=${encoded}` - expect(() => parseRemoteConnection(url)).toThrow(/Unsupported remote connection descriptor version/) - }) - - it('throws when required fields are missing', () => { - const encoded = encode({ v: 1, backend: 'websocket' }) - const url = `https://example.com/page#${REMOTE_CONNECTION_KEY}=${encoded}` - expect(() => parseRemoteConnection(url)).toThrow(/websocket URL/) - }) - - it('throws when payload is not an object', () => { - const encoded = encode(null as unknown as RemoteConnectionInfo) - const url = `https://example.com/page#${REMOTE_CONNECTION_KEY}=${encoded}` - expect(() => parseRemoteConnection(url)).toThrow(/must be an object/) - }) -}) diff --git a/packages/kit/src/client/remote.ts b/packages/kit/src/client/remote.ts deleted file mode 100644 index 196b6ed31..000000000 --- a/packages/kit/src/client/remote.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - connectRemoteDevframe as connectRemoteDevTools, - type ConnectRemoteDevframeOptions as ConnectRemoteDevToolsOptions, - parseRemoteConnection, -} from '@devframes/hub/client' diff --git a/packages/webext/app/background/background.html b/packages/webext/app/background/background.html new file mode 100644 index 000000000..c31d2da99 --- /dev/null +++ b/packages/webext/app/background/background.html @@ -0,0 +1,9 @@ + + + + Vite DevTools Background + + + + + diff --git a/packages/webext/app/panel/App.vue b/packages/webext/app/panel/App.vue new file mode 100644 index 000000000..0d4f1ecc5 --- /dev/null +++ b/packages/webext/app/panel/App.vue @@ -0,0 +1,99 @@ + + + diff --git a/packages/webext/app/panel/devtools-panel.html b/packages/webext/app/panel/devtools-panel.html new file mode 100644 index 000000000..574d748f4 --- /dev/null +++ b/packages/webext/app/panel/devtools-panel.html @@ -0,0 +1,13 @@ + + + + + + Vite DevTools Panel + + + +
+ + + diff --git a/packages/webext/app/panel/inspected-window.test.ts b/packages/webext/app/panel/inspected-window.test.ts new file mode 100644 index 000000000..e84aad6b8 --- /dev/null +++ b/packages/webext/app/panel/inspected-window.test.ts @@ -0,0 +1,53 @@ +import type { DevframeConnection } from 'devframe/client' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { connectionEval, getInspectedWindowConnection } from './inspected-window' + +function stubInspectedWindow(result: DevframeConnection | null) { + vi.stubGlobal('chrome', { + devtools: { + inspectedWindow: { + eval: vi.fn((_expression, callback) => callback(result)), + }, + }, + }) +} + +describe('inspected window metadata', () => { + afterEach(() => { + vi.unstubAllGlobals() + vi.restoreAllMocks() + }) + + it('reads the standard Devframe connection from the inspected page', async () => { + const connection: DevframeConnection = { + connectionMeta: { + backend: 'websocket', + websocket: { path: '__ws' }, + }, + metaBaseUrl: 'http://localhost:5173/__devtools/__connection.json', + authToken: 'trusted-token', + } + stubInspectedWindow(connection) + + await expect(getInspectedWindowConnection()).resolves.toBe(connection) + expect(connectionEval).toContain('__DEVFRAME_CONNECTION__') + }) + + it('returns null until the injected client publishes the connection', async () => { + stubInspectedWindow(null) + + await expect(getInspectedWindowConnection()).resolves.toBeNull() + }) + + it('rejects an incomplete connection snapshot', async () => { + stubInspectedWindow({ + connectionMeta: { + backend: 'websocket', + websocket: 7812, + }, + metaBaseUrl: '', + }) + + await expect(getInspectedWindowConnection()).resolves.toBeNull() + }) +}) diff --git a/packages/webext/app/panel/inspected-window.ts b/packages/webext/app/panel/inspected-window.ts new file mode 100644 index 000000000..5a2039752 --- /dev/null +++ b/packages/webext/app/panel/inspected-window.ts @@ -0,0 +1,21 @@ +import type { DevframeConnection } from 'devframe/client' +import { DEVFRAME_CONNECTION_KEY } from 'devframe/constants' + +export const connectionEval + = `window[${JSON.stringify(DEVFRAME_CONNECTION_KEY)}] || undefined` + +function inspectWindow(): Promise { + return new Promise((resolve) => { + chrome.devtools.inspectedWindow.eval(connectionEval, (connection, exceptionInfo) => { + resolve(exceptionInfo || !connection ? null : connection) + }) + }) +} + +export async function getInspectedWindowConnection(): Promise { + const connection = await inspectWindow() + if (!connection?.connectionMeta || !connection.metaBaseUrl) + return null + + return connection +} diff --git a/packages/webext/app/panel/main.ts b/packages/webext/app/panel/main.ts new file mode 100644 index 000000000..9e0856075 --- /dev/null +++ b/packages/webext/app/panel/main.ts @@ -0,0 +1,15 @@ +import { createApp, h, Suspense } from 'vue' +import App from './App.vue' + +import '@unocss/reset/tailwind.css' +import './styles/main.css' +import 'uno.css' + +const app = createApp({ + render: () => h(Suspense, {}, { + default: () => h(App), + fallback: () => h('div', 'Loading...'), + }), +}) + +app.mount('#app') diff --git a/packages/webext/app/panel/rpc-connection.test.ts b/packages/webext/app/panel/rpc-connection.test.ts new file mode 100644 index 000000000..5a182769f --- /dev/null +++ b/packages/webext/app/panel/rpc-connection.test.ts @@ -0,0 +1,56 @@ +import type { DevToolsRpcClient } from '@vitejs/devtools-kit/client' +import { describe, expect, it, vi } from 'vitest' +import { waitForRpcConnection } from './rpc-connection' + +function createRpc(status: DevToolsRpcClient['status'], connectionError: Error | null = null) { + let listener: ((status: DevToolsRpcClient['status']) => void) | undefined + const rpc = { + status, + connectionError, + events: { + on: vi.fn((_event, callback) => { + listener = callback + return vi.fn() + }), + }, + } as unknown as DevToolsRpcClient + + return { + rpc, + emit(nextStatus: DevToolsRpcClient['status']) { + Object.defineProperty(rpc, 'status', { value: nextStatus }) + listener?.(nextStatus) + }, + } +} + +describe('waitForRpcConnection', () => { + it.each(['connected', 'unauthorized'] as const)('accepts the %s state', async (status) => { + const { rpc } = createRpc(status) + + await expect(waitForRpcConnection(rpc)).resolves.toBeUndefined() + expect(rpc.events.on).not.toHaveBeenCalled() + }) + + it('waits for the initial trust handshake', async () => { + const { rpc, emit } = createRpc('connecting') + const ready = waitForRpcConnection(rpc) + + emit('unauthorized') + + await expect(ready).resolves.toBeUndefined() + }) + + it('surfaces transport failures before mounting the docks', async () => { + const error = new Error('WebSocket connection failed') + const { rpc, emit } = createRpc('connecting', error) + const ready = waitForRpcConnection(rpc) + + emit('error') + + await expect(ready).rejects.toMatchObject({ + message: 'Unable to connect to the Vite DevTools WebSocket (error).', + cause: error, + }) + }) +}) diff --git a/packages/webext/app/panel/rpc-connection.ts b/packages/webext/app/panel/rpc-connection.ts new file mode 100644 index 000000000..4e25d9129 --- /dev/null +++ b/packages/webext/app/panel/rpc-connection.ts @@ -0,0 +1,35 @@ +import type { DevToolsRpcClient } from '@vitejs/devtools-kit/client' + +const READY_STATUSES = new Set(['connected', 'unauthorized']) +const FAILED_STATUSES = new Set(['disconnected', 'error']) + +function createConnectionError(rpc: DevToolsRpcClient): Error { + return new Error( + `Unable to connect to the Vite DevTools WebSocket (${rpc.status}).`, + { cause: rpc.connectionError ?? undefined }, + ) +} + +export async function waitForRpcConnection(rpc: DevToolsRpcClient): Promise { + if (READY_STATUSES.has(rpc.status)) + return + if (FAILED_STATUSES.has(rpc.status)) + throw createConnectionError(rpc) + + await new Promise((resolve, reject) => { + let unsubscribe: (() => void) | undefined + const settle = (status: DevToolsRpcClient['status']) => { + if (READY_STATUSES.has(status)) { + unsubscribe?.() + resolve() + } + else if (FAILED_STATUSES.has(status)) { + unsubscribe?.() + reject(createConnectionError(rpc)) + } + } + + unsubscribe = rpc.events.on('connection:status', settle) + settle(rpc.status) + }) +} diff --git a/packages/webext/app/panel/styles/main.css b/packages/webext/app/panel/styles/main.css new file mode 100644 index 000000000..62167ab0f --- /dev/null +++ b/packages/webext/app/panel/styles/main.css @@ -0,0 +1,13 @@ +html, +body, +#app { + height: 100%; + margin: 0; + padding: 0; +} + +@media (prefers-color-scheme: dark) { + html { + color-scheme: dark; + } +} diff --git a/packages/webext/app/scripts/background.ts b/packages/webext/app/scripts/background.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/webext/app/scripts/content-scripts.ts b/packages/webext/app/scripts/content-scripts.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/webext/app/scripts/devtools-bg.ts b/packages/webext/app/scripts/devtools-bg.ts new file mode 100644 index 000000000..fbda17aee --- /dev/null +++ b/packages/webext/app/scripts/devtools-bg.ts @@ -0,0 +1,58 @@ +import { DEVFRAME_CONNECTION_KEY } from 'devframe/constants' + +const META_CHECK_INTERVAL = 1000 +const META_CHECK_ATTEMPTS = 10 +const META_CHECK_EVAL = `Boolean(window[${JSON.stringify(DEVFRAME_CONNECTION_KEY)}]?.metaBaseUrl)` +const PANEL_TITLE = 'Vite' +const PANEL_ICON = 'icons/128.png' +const PANEL_PAGE = 'dist/app/panel/devtools-panel.html' + +let isPanelCreated = false +let checkId = 0 + +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +function checkConnectionMeta(): Promise { + return new Promise((resolve) => { + chrome.devtools.inspectedWindow.eval(META_CHECK_EVAL, (result, exceptionInfo) => { + resolve(!exceptionInfo && Boolean(result)) + }) + }) +} + +function createPanel() { + if (isPanelCreated) + return + + isPanelCreated = true + chrome.devtools.panels.create(PANEL_TITLE, PANEL_ICON, PANEL_PAGE, () => {}) +} + +async function checkPanel() { + if (isPanelCreated) + return + + const currentCheckId = ++checkId + + for (let attempt = 0; attempt < META_CHECK_ATTEMPTS; attempt++) { + if (await checkConnectionMeta()) { + if (currentCheckId === checkId) + createPanel() + return + } + + if (attempt < META_CHECK_ATTEMPTS - 1) + await sleep(META_CHECK_INTERVAL) + + if (currentCheckId !== checkId) + return + } +} + +chrome.devtools.network.onNavigated.addListener(() => { + checkPanel() +}) + +checkPanel() diff --git a/packages/webext/icons/128-gray.png b/packages/webext/icons/128-gray.png deleted file mode 100644 index 2ee4a68dd05b6b52a97e6fab142a6f7d2ed5413c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7612 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 Wawbpv(*=JW3uvk7t2QXxMgJc@?m=q+ diff --git a/packages/webext/icons/128-inactive.png b/packages/webext/icons/128-inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..0f1e9ea88dc390e763f953eebb400482d4df7f1e GIT binary patch literal 12582 zcmV+>G1<2b@yPK*3C5XqsKO3V-i@${3#z7<0z{=SlAsF6AC#7Y0#fBFwhVFQj|8m} z#|egjLU1gS7{-nh%S6`hYIo<=_nwpQ_kG{F_jd2>YG*Z%9_<|I?Cr-)Pj~;m_v;WH ztb=v14%WfCL6@UlTnh_}N6HQVNI|nlL!3Jjn!{AlVJhOyRK{Cm7jpk5@^pv-&5}<8 zev>7+#G4;=&fDAOooH{C>;ACIxAB&IlKW0>M{?MK?+08P@?}jFcvtIu9Z@XSs0pil zy+lp1!nZF`419Gf6y2KqjPoP%{5n23D$*=}CxcUT=BH17_KaJmZCC~E;wnb;AAPEh zdK&O|HN$^=hC-U-4|Yhd3ivF4zcUPVrNHw~IerG<2BJFwJQ>UMgM%*@I#1mY!PMHLIiVi74Qk1kDsaJt$7fD zQv!elNc?RJ@9(7qFd}gM`z_}u<9yxx!Gs9BaDUAl&zukN2Wu3;?WJ-*xL!GbgI`BE z-|=gK`wJ$3g4y2(FW11^)q%Ea?YIQ^N3Te?FrhyR&JSszz^A$KO-c|@3oe2CO95Xv zUqrtb(DxjCTTb7Ifp7PNay}XOA^`++29&p}!1b)t2Mlz{Kr;~#6F`LMhYDcMcLAKw zV!ucXytiG*F71jPpSo@NGhcR((>AOfmjM6$wa11m&gXp`v|Jzh;-BW^aR=!@3O(#XKO;aFv6BD==@pzW;-7Ut ztbOkkd?(=RYq{RPd$J&T05YZ42~p9nz>D}d?)M?>!8fDN_eDS4J_a759{Qgk0f>pfDFOJb_l5ht?Q!tyKYrQi zPk;WUL)yl*V+2rodXSB|c>zC~=+Ew+8GhpSIR-zf%jccBd`b1bX#Ks``F)?ir9kC; z%2q!NzSiyID-1tKfKqJ$5J40VfB}aa$OI4xz+>0K`FJe>-w2>!7XVj?eSqIwxMlh2 zU-^qCXq(rL5P-ORR4%mK&p>B!KkoCJ)lVw=MeI8@{M?=^eOo5_v%#lEUm|^*rvDMB zDQp|~VgOd1KfmYXWRo`ypRV@5hUW>B4WlX{k%OBKq{38MEnQU z`H58QJJ$P2bie9-l>lA?KVG>Xd;PoL`|$n$=H;ho2iCR|K=;Rgvo*!&pP zvX%3b5I_bzgOBfE{p+t>{?4!e(2#a$Z7TtM@JBxWU|^RoWS6fM`{RDUcY`+I1MXMR zZxO%#+H3H~B7doK{p`Lo6+rB}JK32CYH|HC5u`$iAd+!E!RqJdD2sll+l;?V2Q?hGWh(VIr|}^ zFtq_6nvUiA&WHi-dyzibWHx|G03`@@|57mU-AHtPYPnzCK5)5r5dGs~AN#3aeaFd9 zfByTHX$RM~L;|7r*=Ah|%-I0Aa16ei(+%JqHiPd68s~?Io$CJ5xLyeW1i{3?s=@hE^gcc?HT+=_<0m&3@JT&)t0e}xyNeWoOkbk0 z_vO5K04DyuNP~U=$W#Fc0N@Mo%JnggI^CZdaVa&k4GG{y3S{u>XKr3T_t-thXcyLV zCV&t9=;EiCqZh^S1NUd(Jre=cfDnX&@4fo^K5BADtmvlaUuV3dUey>>AY_3M(io-e z+``}S{QywvwpahZ1>eNJ>{7ue<9>kOfb&5BNkxA$-S1k^v-ER2>hf*2o)ZDw$5}p( z-aW*IA6x&%`78xW=J+zsO1__ym;89eHi5?X1$-;&knfk#j`A1--Qe-0a#8GV%e*Dm zs)J5CxN(BmiG@F9dHy!})B64$_?rGzy^p|7^0n%EzTG4h{d(1quXq!rfCmG=75(3R z;f}d{o2}x?0442ELIi69gmxiaeaYp!52oE%@y6 z{mGZ_8PYDU>n4Ev{?f;n*r=O_ZZAo}uX@0jZ0HpLpYH{U?C}SV>g`jC{J({HBSgHH zD@E4{00TU!E)aEqt4m!@AoW4tPC}T7{b#qYOa4$R0aO(j0Tjyh$*H~<@FNI-??(ln zg?#JjWW6t<|I%UdE4S>Z%eUFOE&}-Q!cQ#(R}2MwaK4|R3R|Rr?!p0l&R@g8Nja|1 z93q>6Z5-|@qcg`t{LABT2crU>jA&Y7Xr%{r^#QiBu|CGR*Gmj-<}cq(X04kFaIsSj zP6Qyta12KWG42!XF58u;F>N({;QRx&^yWXX}T8ZnTC zcqZTM_1Xw-T)oKq&Q(8Sc7PRuNHRzDe-v)tpxWsM_czT*2&1)biN*}Sjc>m4%ywt_ zu61ph060OhRQPJXG6KLJ1O>k;m;eHce%ASgv%m)7I|C4n04X^i04|%suAN?^N(E65 zH77s-v`B?yk2?z%xsC|pIJntt6x`bf&9&Hy$6yjb2sKyw;#JJ}BO{0`{<8!SPLJyT z0r(n(0R10pApkF?jn6toKg)$9%NKwB-N$H;)|Lq1qaXRHPq9(A2#|#U81RAYN_NG& z3=BTYIrz3PcH}cd>;q&FL1Wpw_=#tYC9pd-N2sA>QqxY`ILnlx}q05GDG zNeVR-Q(G}C#L$9n4*eG0#%%aycQluRIoFCX3+mLW`$-MIIw|cxIzP<8$ zl29%uJqe)tAHN4;4gr!y-%In|TOYpv-@WP-?cus+0${QBXjRM{ z0nh^gUsX&11Gx^$lFE{01Im|C#^lwK_hRujCIuAu7Tmhyd?5sO1s&7@h`@-$HQt`* z$6561c1j3Q2?1(Cb+9udGJ6MUJXUnQ0i-Sjp)W(!>3vJ*BC#V@zlnU&|Fq5o098<1 z^pD?9sb9BP{olNE`Qqd68PXoFYa)P;{lv$YicroM3_NPntBM^x-qnCNf2hF#b?dyD z?fGd1-+~?4=U4F!ksd|wyTUl%YycsKT2+8Z0$fMt9R#xzY9p2=brQiDF*q|Go!Sm& zGIsC5rIs+-9)l~b%?c6n%aDqHiCHlBi&_KsH>UT=h#;x&-$+168$6&2g%MV6rqSAw zU3d95ThB@WiyvJ)$_NY#;qpSjFDn-H18mPkz#<>uS14p85pe>(%l^Fhd1CxT6ZbKL z-2H066AfH>&gNYzKem* z1wgI{K}X|$P4)o%+AHT{kCH|K5f+vpms3KgKfdD(gFmDxSyv?h{!A?T{XA;Z(2yVn z3()c4VE#>C=4cqKTn5GE2p8*m@&BPC%^(nl^PL6nR01Fx2Jj*BMa<)80VcQw5@F=c zGf>b)0@PltYTU=)_27JzlL<)XW{G`)jv6KqsA-qo#2yh`#W2j7Kxf>aQkDWH#?_+b z_so)@QR5GQk4SfI!5I1eDz?@n=DEuiKvqVSS=w;5<%~7OwyP}L^(6r$d=eXK7Smw zE?<@aLJ$$@;70y-QNMs@zP+%gF5hNrQv$f}zWe4`Y7IqWX(UIC9$38M>TqJGX_(T1 zEsJDOI(%KJVyzV@lz4fO2)VxBn~(0a>^l=ie%|Y2QYPW7EIelp@mhd@Uq4Fa46cn( zWU32IuNrrXdj_JAT$6!pRmQ@#y|78b=Unc1j4SF_4HH z|NYvv$pX^$GK&u)8_D-zK+4MHKCOG$yWRH|TDMWZ3&=Hmb}>R(^))R1#n=PK!~Kh$ z@fR)mGkfl?<=H1ca)MclrfpqD03ZJFhnM&=pDb$?h00l*H<_DA2%=yx>UyMID_-l@ z0f+$a6uUJ(_14gAz90T2L^;7>;YaQ}*p7trj* zyG~4l%eUDYBY+Qn@PkL$PajH|o`9gq4gifWuwi15&0IgSjum{VW5tY4>->bTeXjiJ z6T}3uWWva{K~ixq(jW!WPNCc)4!w`JT*}p z!gpE3{x#p$)I?huB&l4|*N^%8pw9IhfdIhyP?hE;&aS;=`GNoRL#Jsx;O$PoKtIw!GrAORvj=2`4Dp%H4=Y?d}LD7aSI+;X{z{#@!JARv{c=aW~5(6@?;71Fh=m!R9_`m|71z0lZ z&I%gxV`|{{p0|QWcNr1b&lu-tV(HXDUpgoOc+N6PbrES%C_5%srHcV(;59!bmQUq)~;E%pPhtz;_z*h0+o5d!mG61ZQcAmknWJgFB8L7}5UI!c)Ea#)*ge9aUL|as({E!09WkJ$04|>r0PKxQ z`U(n<+1=sS>&hGWdWTUeVq}g;y?K) zuOFQmgw7sI$r6v&+QxspE-uc~6fYqF27W0&kipLxtA6#$m98BT&}EOc1cArRHju;( zDfr@Mx$F)a;bnKRRShUCL5p-LC1xl_Q6iY(y&=V?aJj&d-sR>E6qz^;10!ybG!h5D zTpq*Eh^8>91saK6dAfcXVg)G?m`V|;h+lP@oQfa>ua9Lz)@dn>NQ{la|GRs5G##0C zBXB-Hki~umbdnIZdOOo1WS2EKub8l0XQcU@TE@ur3myEEs%ty-F6r zB`R@Ul&`(3+vC?Ml(i@t4Y)5CT9dg3%>BhRU&pKS;jkWxR8tOKd%Vd#f2XFt~*>Zk6x zfk|rpB<(99?~&I0$zZ3h_CGl*`O0cA8_7)kO?CF@58w7rmLL0rpPxE|P`-sb@4WLd zzAW%QvXx(+R27rNfhFnR-tBz9y(Tl9?0(Ze-`L}g{hk3XNth^vl5%)9>RcU>8q|3e z;zo)K*i2;keNTmj@V@QtbzlRVyBbm%$Fq^?sFuikRqmQLc5kz;v`C6FxEB6P>^!CD0_&)(h6! z7l_xXOOByjIKzKzS^bJ=Y9{NVE+U2R1dP>~gzPg;UDqvRVf7j9*%trYxfj&$_}8bt z_?gEY?W2WQ1ry1T_3TfA01$dE0~0_sM}y)f-iK%q4ogRJ2||d6SZd$Iq3by!CIIX- z8Zfmn5sWGh8kJg8(3pCNZp;QyJ3zCY($#~xKAu4i2DrbZV_rQ5J+8{=mn3arHWC(g zNm1MHZ%Bz)bQ_|+q*@5Ig86Us!tOcNkfkcX#u5qHBZkS{U(ZGigLo^wboE2`Zr{Yg zU06>%@x-~WeeG+fU-!D#zuZG2_#B1?F_dZ`26Yp23p!#45;Wpqk>f0-MRTWJ^YOPei3@P zm9BkU|FWg8dT#pVK{?v=zVAhCv5$=2d}d!t0Ib57j|=$aK)%GP%zX)!G^7>`w3mvY zpp{0}j-Hc0&Wt<@-u3<&&k2YSFz%OlLM$ZG==1BqfD{$8Ar(-l5A+!MD&yZcUGD1E zeUzC&A-j=wwTfaim4M@=vbfRPa-VOH>J3TAjo21|J%G?g-@xv3xx`jXGEAz0{5*a> z#6#z1>84}*mH=jEX3v3>X39ARe@-Mo?obl_-`Kd>q$%fQ6-x32xX$HslZ@ZGg#Em5 zRs-2f(EbDoSiufq@L*p6e58s!hyb+`h)m)+QeyC!|6gePxOfV_-$<%yWE7YP;7ta7 zT_U00b+Z%erWH;0cE*EuYHPSFMsLgdMf9@s$4)-*U*5F5ZwcVg%;1P;(LZ4D2h|}V zgwp9iihRffVd}UmL`O&0ji19xCeAla2agSh%3{|E_sa|bs1dTp)uWj<{d~7$*xjU$iPFgZH)5FL=AGmJ}9gn0YNcIuE`tjYyo+@&W%N1 zHzR~w)?N?rAHwZ@O916y?)`&mmR&%0{Yv)#!<^&>kQ9QOhtx0ti$VZJ7a25N4~%2q zKFW;VN`T1)0*Ycck>0`tEsrDrS42Q#c zSI;cY4i1R~r~(YYIW_uN^hf6WC2Ox9*);<*F4trW^q!&=fL zy{zeE>3A9$*g4d&?%OJ5tu0KVGH{NNbVu>SH43o!@tQ#&@$c8zbt9I4l;l{H>?xLr z`#fKG0(5mJ!`Nek$AA3f(idNO*&!4AiU1xMJ~+=GZa6o4lZJl>bCMR8Lna#4`I_}B zy5x`f`zeODy?m_Ie6x1)EA|a`1s{D$A+-QfwX9Q)v~oE2!-PBvN&5?I!+=B%DULMC zsPn^W+Gw<-e0?;cjh^e*G||Gt`fjxY%Z)`DGw`8zz=U`N=U(1s+et^Ia8-3pEykV&X5Le6_*vj=WcN{goHQG|_>Hci=T}cJ>SFcV$f4d_T z=@RqQ8gcTN*H}YCmRL&GyNEWoN}oqe0p;V;QH*{9_5?QKI)l&GMt2>x?oj@wqqg$8 zD8_;BUt8{f^3N@=|IYh{M1OVDkF;+H;7}2k%3^jNdLKF;JJ<;$!ubFkv3}_?Mh_u- z05x{dzQH_z!X}SoLdfc0R^JW0O5S%80DsDAy)hBZ_90Z9oRf$k-i} zDI!b&Be%|O;2IObx)8xghD>OsQ(G;CxPx)Mo*@+4vxFw4fUjrIjeh5SSM5UH7X-kf ze=&sWI4pkXeC%LH%fTlXE(h2f)>1A6ptq%U4EIeMNY9|n7?K@(MqpmoFhn9(3z=5X za`R;5;A1UYBY^1 zR!5;(Hv^uJlxvett4FB$+Mhr6Y^`WhPXH|X=e;kN2F0AXe7PP_gE8RvLO8r(@S72? zLAG=cXF|w1|D5+vUB@zw>?7d7HG8G1!4Q-mS9!Ul#(v5pqxonm^DZW8v*#0*eA( z^oU@5XT4v4=l0Y0|I4=wujOM*H36Lb+0QKnKXX(vd`?dnK}JuFy&(EsxPCozzTIBO zdswFEHy*$3wN;}0dg z>-1PTCk-En^r?8q7SI9ydUqAEZ}vbm{h!4@*~qz=db&R)O6huuz*e>d1$fIkI-4ON zh8)Vlm;i8U2*iC{6W|+C@EeqqIR#%PkC%38XsOmdCbMLbKPmPz=i`8hwev4|*H&84 zr;-4M$A?GaM)~N>EV6rYVz!F>kv4*6?pH&v13&f%A&%^4Y`l!x0KsekqJ9NXTMOXQ zb-rUq)T`-Bxn>-J8=u?iwR^bn;CdGO4TJ7iSoE)GnHkO#z@gD{=1>ywW$uyyAKKYW zn_2u46dKLYh2Q+a6ZikZ)<(}vB>}9bnWY(5&C6kuY0xEiT**-~4eq4I02dWdC^4+- z{bYK6G}0uo+FIIqyT;Z z_~807=;XDkGxYLx%-XlO|JswcJSNd#9*ACX9)FzK&=PRtN$Cb(61MKtr%(be;ytwW898Byc~HJFEs0jKD13M+!cGPg&oaY8V4QdIQ^yiT-vrfZn-!6!1!`?MUf3 zKea=nA=X*svz0H#p1UNRFOq=m0GuH#$H|tpaz22syaC_o`@X-Qb$;H>GdC|k^ILCu z7hUI?E(x&iA1MYi^X-As4d?bHty{6>tgfGMzxn>rEPnyH@AKCaoS*CdlBa&=Z_M!9 zD)_F;BB>jgv`N&)=-^zy1}B8NHMRJIa(yh708+Us$H>An5YFe1$D!^P_&xzlx_O!N zn~R5O_4i+K?6c2d?$Fc_z)yel@8Zy@qlJO*<1BxvGYqK4`w{6A7qAJPjvv|JNa1{) z3Z~P?C-uJX7?srMb6br3jjgfNft6~T=+{mm=zIeo-0zL^3j@DW@NudbP8csrGB?ln zz^}%9{z>q0efqJtpS=I`-*<+d!ADG5!gCuY)4YMKko$bJ6*&;xKnFgP*#A(FpJW{MxbZ586Ni?vI7)eP1#7 zgD8_#OPjPh802A`)tJto%OIRUIzqoskn zi6sDo@0t7KA^wu}Y++^-1|4~RHUMiS0FuCt4wII2uN7wY-Mkn((8tQY@_n-VTU|K| z*hP?31tdEoD1%>UfTv;3Z=mx9{1s^ub=8J|A1bzNVDMF8O!v3F1QP_1SK1hyJNs+z z+_pKxdrANw|G=jg`J*4lq0(~H920?q@yC(B#PR;fF@VTkfFypz|F?8+&$ts&U;ECj z>u2}UCgT3O{r%49(`L@j=+klzMDfrv4$;42Tz^q?egycXaX-Lky^k5o3VtDjxBJ$9 z7WsYd$HMsn=YR8!C$@9g=v48EMKlLxMc^ShgQPLQ)nsi|%7(D=TJ{YwJ6>U{>k0_U&u2Iof; z`(6V*GJhFe&YN?7zoK{j+b=nJVw;bY+cN?Pe(+#O0~Y-ZzEi#LoPtkQ$O(f_8akn3 zV?IV^LbT@yTH4s=FN?t66`^w zk23hyGA=U&R+aNhHu}K*8T^*>y9{3z{kTq!+8I6dy~}66{Db$>4y;|50E-Ksn0Kxm zO6xY+iK4LZG55D^+l^8VEw#}^cD}F;<_YT8ixG&Q4M``1k+$) zEy#e%^Q76kE0=&7fjH|2aGlF1EnQN)u_lqdT4MX^?@OQuk-ox=KC$=}d`a-Y;@>35 z({J9CbA&G(qIhBU-klmUz3T)pN}T1h{&`gH<|0ZtO*^ni#Sv>VC|b|wb25G08ap^e znns1n^N?o?OO>P@O6A)&Zl9v51wUc<*&LrX9zdIC0+Z`W4AnJS?Q5bxp9XG5pA3u$ z+2YrM5w2J_&X1<^jR3&?o49)YI(~Vl>m{B2(pyg4|Lf5ISv-S+xmX(qS7U^5=XWAgxutXs~ek*!g6jlQ(-IcktTp$V4zlI%$+FCqO0z z(pZBAR%-ttn8p1x|NpPNc}P3Fc7*^I z7Vev8USCqFYrxB~>T3c5_#Xowk?+T06OOvpZjl7m-=|Z-z4h5y#}DP!={!?d%fsb$ zytg3`!CJ($o?dzed}!@~c4V9#TAg|wJaXUZk?)XHWYS8qeBOdRt=?aeFfX{@R~G4$ ziXP4H7gvbwPjY`+A1t5$%A0rPP^n!Z0JhVX*wC5>7ezT-!J7_l?6@_~_My_ZDaLFA z?TFRs8#;kgCy5J~q{qkLF#)?U;FC@im*O>7*xtVfv_P8?aFmUdtV5FL(BMGnKX`r9biv*YbBFl574)xadJ(cNUe2E?;_S8 zR?OYIq*KD6_mSS62==u3{E1?Y%Q&CEm-GMau9GKDy?WQi%kK&SG^sgU^7?l{w#zjUhRZ+9X2i5ko3WQWWiEak-x-g$WUq(ncm_bU}Pa(Ss|3*d?tGISDw2-wJU4F{f3fq|>XWUo?_PGm!*LiQ%2)$(KTBZHh) z+klU9zZ!k0>+==3zY*|Dw)(45#XfX@m~i>DsZ;HatZr7n{>tUEzxwuV$?$DuT~Aiz zx)z2!qRgBBb02Af~ zl1c%Tlw)`lqje8Q9-Yn;;7BB=o#lBZ1{Y_nrMw#r?Oh`R^}^*+rHdOCim9sXj_peA;O50aO7ROC?T$^Ny4D?MY2vct*y<*425Y6$Zar zA;;h&&R;V9Aj>zH(W7+-gKEZ$i`oB0ds!k+LRY0+2aOlQr%O|Bmp1XKbWcPeC0~2d-|?l zzp4jo<@aB8@{@o6?L*q5wJ!%nZpZqKC%*8VyI=c`6$;G{Cp+Y+4fas(<{3%Ner$96 zR@W!>eXE-0<$w=FzeM^5TG&xZWlwdP*q?OyB*XU)-u~mXhigx=fzz`7#p%!dv-Rfe z*>yTgBZqyS>VoyVV=>R1j@`J6ObQo8_e*i7v%0=Q>-mckRzI1=?|Y#8O?7_W8-Mb` z9n0`-W$oKBaoe@l(#0=SuKr0m0W8%r*bymeQ3Epc35Qc7ScSH2P4OZM=nC!RA@uPT z3Vzv!d9Up2V`+wO?%v%B_FQe<@C3lzy(}k=IUUp-WW4K!#tNLC`y+!IiVe&9t!X`f z&?Zhv^b}Vf*RmP+pMCUgDCgd>4BuAP4Nm}(Fp(o^O@Oz_9Wv3gk~l<^;a`o!_|e!& zVMBH(qNEe_G`e_ zi5|@R4UG(a5a7|jAG#=Cv5CPaufNUZ+}pJd2q5Z-AehsOz*1u!gXlw~%(-5Vh(!sQ zg0E*36@`vzE8A$F-?XqpqJ7(3&b?jhh9>|C_1cNx81ddDIe1RaACxixB(Fdg?UZjF z+XC+QK^r-}9H8wtZQIri-%a$)e|_VLX)yXG*8u?#S)qrXNERef<=Y}KO<3sZpx}Na z6Lnxyp4R2B*u|6A%J02m+snDPYaI{(QG0xCPR!^j;aXWN$HzKz_d*hh0KZ>>S5aSF zp{@_t)&G0@nZNl@-!P;pT{k=dNcNEQ@F^MJYQz6zx(7uo9dE=Okip-HFc_tHzwccB8`n9oX)o4+3P6>RoG4a{^{>q!I^s=^ zf5}t29kqv2hKcs=*57~T@bZP{FtBMa)`1&HWzr#!>HaA~4{0B+gJe+Mf!-t?_<+!_ z5P%db2i5e$$0%p@Hc`*-m2Tx|Q9vR&siRBm z^mIRLhSBV~wXeK;>N0#=SqBwCaYI|aq8`s8-hj7hB5f5{zkAoNtGCd`@7^<&8NRKo z0|`)XxF*>t-l77calhpQP5ftS(18|g^*?pn$;LUOg4%+}pJdBtSIqDQ3iN9$KaqEOTU_Rjl} z0TsP5^M{{4y8MD~ofy*da2==s@d9FRPN4Pd+gMspH!t6M?jxw|*|))-tF41ckbH2roC7PHb9DM z)l2kO2UcNV)Xk&2Pygdze&>*$=j(=`;(2kT%Rtb=v1p5N&-2eap literal 0 HcmV?d00001 diff --git a/packages/webext/icons/128.png b/packages/webext/icons/128.png index 06060874523cf18da2e5e85df65e05f42df36c7a..6b170155a6c0ce99f43f623111a38498782a5091 100644 GIT binary patch literal 9618 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 diff --git a/packages/webext/icons/16-gray.png b/packages/webext/icons/16-gray.png deleted file mode 100644 index 8f9b39da4dae350d1e47c2739dc03c9449efef80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 697 zcmV;q0!ICbP)Px%b4f%&R5(wSlg(?BK@i4gw`kM67Gez5v^n%6gn;Qq=)sd-1+f=t4|?z*c<^8^ zFG3(Rfwq*Su}~3e!Ga!Q^`J%3ga3dRFM*cQq*iECu{6*?A~tV!W@KL4&`LxcSXgFf zfAc*1p`b-Dvn~`0L9KO|hz6PYlGb{Nh?b+#XyW)5mCNNWGV>`!{O<91DkCE!&ieX# z4oK#`FMy5kQKF?i0~20DLpXtoi-^dT(#wd{Kt!-C3nGySXstm2N-3yRDo`vI zq0wkStyTjfS|g%iCG^F`#e2;B1QFW=(An7u;c&PKkQ!r*J2p2rVP|KDnfXB?k$CI^ zW@l%8mSu^Gs{-)(e9+(D?@B+iAoFUq3i*5<4h{}JGxLpPGFf&367%!(!^W5#B6dwXA*`C2BE zDgN2;#2Fl4jK|}*nfVzJohG7(nM@|#O6^|($z<|OtyYr{CBIU-|M)L}6x+6ag^12( fv)ScVn$ zh~O!Lx=9pP5Y2*uvX@oCiwFG)d-S-!KsE(VuqW0I8HWqk4ViCEaEahf{dGN;e%)wAOcqsjq)Z(I9AzWelQIQ~lUDP=+3tjZ z8_UmXLG9l*C12%Wh|#+?7H;>#xe6@2Z?{gX=b0+R_!XRuFEW)B>R zOnXG@;+q?V9j+^FU?8YVOg;!ai9|3sgapiU*PJ0}T<+9p_wWvvev&QOBtu!4Yscjx z2}=hG;O!&orE6+Q<$nRiz|tq9w*9gI00003Z(>)8Gi-<001BJ|6u?C010qNS#tmY3ljhU3ljkVnw%H_0004VQb$4n zuFf3k00004XF*Lt006O%3;baP0000EbVXQnQ*UN;cVTj607hwNZDF3VmSX?_0ia1l zK~#7FwNg7y!$1&yV;dCiKw*wRM~i6a_+5Y-5IUg2Q3{YK0)I6U0wK`?4J`#1;0QSZ zO2sGf?(k-JqnHQ@C>Y6J?|9yP-Wx%(^G%(9d&Lt9ln^>*0L=i$?`#$rYP^Gw);Qa{ zkv8Hr+Q%!pJW4Q7%xeIGg^8R6=78tj1O4||RAXF7aJHg5ZisCwEI$5Fd(q)|mm((1 zi-1_JE@d!Ikbn10C$B{%ptVdW-+G ZPg>@p#SjpFz{mgq002ovPDHLkV1khh==1;p literal 1335 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 Xld6$e_+S8K00000NkvXXu0mjf0Xv4t diff --git a/packages/webext/icons/48-inactive.png b/packages/webext/icons/48-inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..43d32bbcb1e8dc0a784b90ae0d177abaef48b1e7 GIT binary patch literal 2450 zcmV;D32pX?P)xawF|uVP6%2hFv%!*@hI*^e5enQcwmf$#3W!%8?3j6s|ks}ifaYp-~)&Y z@5vsl@HM%&#@`2sakns1zw7e3i@%uO2%DEjfva!4@}H5ySlJ{#0xSqfUIp-Q2P@b4 zarjbvGw%c*co870N)X5!xaz#eyw@3cf(lv_r{L*tn&E4ee%Llu;Gv(KUD&th_Pf9a zh^YvkE%45Z!Q*v)j0Jh8$Dpl(x2DCQ1pJzScS_)H$?EEzPRrBtNOoPFKHU#r(C_Bq z!{7Wm+Oj4VCItfZKtX;-;0Et8X#7k->6*OS2yhA?zkxe~HyA=jm2iu}w*)*{pA^?8 z6k6rQ$1i>7@~{egX6l~9f9F_cfW9ce%a=GO@QgL#m!l8xUXof+6lkgkTB3;G6i?_9 z%eORiEdP*Z)(`8I#if6oi6Dpg>n~KRa}~7YKYhP2 zBFRZ#@gz~L?ylN6e{iTIS@Ny#4{`LIDUp)#r zWm2aPZ^s52`s+aIAw^hN2;{6x&!P zSqZE}WKmRrPGH&zloyMI>2Nb9#Qc!+!FsXgM3N?UDkgu)?59BGzWb&wkTr}W7Zx4TI>$^mQJ_#G;tEr`+T9$6JVAbAj@5|WUH54J!Q04bwlbiz6CWzW+C*} zD6CGrkgH_&RA6>?_K_VsMox8Kc50AEoJ$Z4a~!7#0-~@&6ZoS;1(o&U>kBN$S=VzD^qK&Q1}ohs>%BWHYw#Tw5oAz{999pX1enI~5p z7gC;5kwCz+5(ZkutEIV9b0rv3f_A(8uP_Y1+X-33d_1& zQW{fFFa~}+&V7RG4BH7CF^S?V1^S)`cNzJlqwuY}Qql)kWRZr=cIf;5_|FkzMOF(dUe+QHZHHL=D% zm0B<4vm?eFB5Lk<6X>C406M(jPNQxqHuhio)8Brxv}F(6dFN+Od}`0WvMZm`8yHPg z^?=QK2ey73f>CeKJOM9kWXyB2QwVd2L2MUlZ z=Szqf%?G20*?NE`i=AFkgq54XFLhoUN`Y#%TG=_ib3yV>Lnkh12o=PVoCEY(s*LNI zCCQCeEQOl(!7h>YdWRI&_C5Q=6U(*rScVCqQggNa%*5D)$tsPMK%+gfOGf5FUZUY6 z+{e1P5Ai7K%qRz3&jFo^bc&6}8%K`!0yD4zCw_c#p%{h-xIDTtnL5&NJx?e(F)EbVI{cz<@ z;>pR$3;3prwfX|1$!Ems3`WN#43OuYb)9{ZwQq1!A9JrP^Pxniap_6iQH6r$-e~Vj zkJUEAmCFM&GcyaN((Yqve_(ZRr9CrwBB4PAbp`HlqQeZ8V*1Q>l8#O0K~42QD~)|? zx)L{v!?&hum(8`!u;o&qTrQt-pRJZTW2LciLoV*heCpO>QGy0tl)#?Uepc3NXOjxk zow%K$Q+{K1{;_|5Bg%ssY)b<P0?3%`P(&s{J&mw}q1UY5P{XjqY(fvPtdiSoq_Z!DAwebgMeWPO)a6_#-h+!Td zb^F0CUqj+g&nKhB=6As{;3Y<;$<~NTp*EmyI5?m?wsb=`=m) zP>bZIG&Frh$0*rULgU^-s>Il}!;61-`iBt=A=|G2K-c&%4H?IvkzpeEB^Xk+HH1iP z$}UxuY0@@QKiHFD4kFvGKqj_hV=bc5_7?}LIKAW+gityn2B(CI!1OgmOuJ-bZ QNdN!<07*qoM6N<$f^HLycmMzZ literal 0 HcmV?d00001 diff --git a/packages/webext/icons/48.png b/packages/webext/icons/48.png index baadcfdb2f868e9ab44c7bc6506022e1b6dc39f1..67a464e748f5ad1f775917462d998c25fb72004b 100644 GIT binary patch delta 2340 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 KMNUMnLSTZY3U~bg literal 2966 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" - ] + "16": "icons/16-inactive.png", + "48": "icons/48-inactive.png", + "128": "icons/128-inactive.png" } - ], + }, "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" }, "description": "DevTools browser extension for Vite", - "devtools_page": "pages/devtools-background.html", - "host_permissions": [ - "" - ], + "devtools_page": "app/background/background.html", "icons": { "16": "icons/16.png", "48": "icons/48.png", @@ -37,21 +19,6 @@ }, "manifest_version": 3, "name": "Vite DevTools", - "permissions": [ - "scripting" - ], "version": "0.0.0", - "version_name": "0.0.0", - "web_accessible_resources": [ - { - "extension_ids": [], - "matches": [ - "" - ], - "resources": [ - "dist/*", - "pages/*" - ] - } - ] + "version_name": "0.0.0" } diff --git a/packages/webext/package.json b/packages/webext/package.json index 03e86fcec..5d9b66509 100644 --- a/packages/webext/package.json +++ b/packages/webext/package.json @@ -20,15 +20,26 @@ ], "sideEffects": false, "scripts": { - "build:extension": "tsx scripts/build-app.ts && tsdown", - "watch": "tsdown --watch", - "prepack": "pnpm build:extension" + "build": "pnpm run build:scripts && pnpm run build:client", + "build:scripts": "tsdown", + "build:client": "vite build", + "watch": "tsdown --watch && vite build --watch", + "prepack": "pnpm build" }, "dependencies": { + "@vitejs/devtools": "workspace:*", + "@vitejs/devtools-kit": "workspace:*", + "devframe": "catalog:deps", "webext-bridge": "catalog:deps" }, "devDependencies": { + "@types/chrome": "catalog:types", + "@vitejs/plugin-vue": "catalog:build", + "archiver": "catalog:build", "tsdown": "catalog:build", - "tsx": "catalog:build" + "tsx": "catalog:build", + "unocss": "catalog:build", + "vite": "catalog:build", + "vue": "catalog:deps" } } diff --git a/packages/webext/pages/devtools-background.html b/packages/webext/pages/devtools-background.html deleted file mode 100644 index 7596585ad..000000000 --- a/packages/webext/pages/devtools-background.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/packages/webext/pages/devtools-panel.html b/packages/webext/pages/devtools-panel.html deleted file mode 100644 index 11ad59586..000000000 --- a/packages/webext/pages/devtools-panel.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - -
-
- - - diff --git a/packages/webext/popups/not-found.html b/packages/webext/popups/not-found.html deleted file mode 100644 index 081a0ee45..000000000 --- a/packages/webext/popups/not-found.html +++ /dev/null @@ -1,5 +0,0 @@ - - -

- Vite not detected -

diff --git a/packages/webext/scripts/build-app.ts b/packages/webext/scripts/build-app.ts deleted file mode 100644 index f7fa0e4a6..000000000 --- a/packages/webext/scripts/build-app.ts +++ /dev/null @@ -1,102 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { buildNuxt, loadNuxt, useNuxt, writeTypes } from '@nuxt/kit' - -const __dirname = fileURLToPath(new URL('.', import.meta.url)) -const root = path.resolve(__dirname, '../../devtools-rolldown/src') -const buildDir = path.resolve(__dirname, '../dist/nuxt') -const distDir = path.resolve(__dirname, '../dist') - -async function prepareTypes() { - const nuxt = await loadNuxt({ - cwd: root, - dev: false, - overrides: { - _prepare: true, - }, - }) - - await writeTypes(nuxt) -} - -export async function buildDevToolsApp(buildDir: string): Promise { - fs.rmSync(distDir, { recursive: true, force: true }) - await prepareTypes() - - const nuxt = await loadNuxt({ - cwd: root, - dev: false, - overrides: { - buildDir, - ssr: false, - router: { - options: { hashMode: true }, - }, - experimental: { - appManifest: false, - }, - vite: { - plugins: [ - { - name: 'mount-app-dynamically', - enforce: 'pre', - transform(code, id) { - if (/nuxt(?:3|-nightly)?\/.*\/entry\./.test(id)) { - return `${code - .replace(/vueAppPromise = entry\(\)\.catch\(\(error\) => \{[\s\S]*?\}\);/g, '') - .replace(/export default \(ssrContext\) => entry\(ssrContext\)/g, '') - }export const mountApp = entry;` - } - }, - }, - ], - }, - nitro: { - preset: '', - }, - hooks: { - // skip build nitro server - 'vite:compiled': function () { - const nuxt = useNuxt() - nuxt.hooks.removeAllHooks() - }, - }, - }, - }) - - return new Promise((resolve, reject) => { - nuxt.hooks.hook('vite:extend', ({ config }) => { - config.build = { - ...config.build, - lib: { - // @ts-expect-error skip type check - entry: config.build.rollupOptions.input.entry, - name: 'devtools-app', - fileName: () => `devtools-app.js`, - cssFileName: `devtools-app.css`, - formats: ['es'], - }, - rollupOptions: { - ...config.build?.rollupOptions, - output: { - assetFileNames: 'devtools-app.[ext]', - }, - }, - } - }) - - buildNuxt(nuxt).then(() => { - resolve() - fs.renameSync(path.resolve(buildDir, 'dist/client'), path.resolve(buildDir, '..', 'app')) - fs.rmSync(path.resolve(distDir, 'nuxt'), { recursive: true, force: true }) - }).catch((err) => { - console.error('build devtools app failed:', err) - if (!err.toString().includes('_stop_')) { - reject(err) - } - }) - }).finally(() => nuxt.close()) -} - -buildDevToolsApp(buildDir) diff --git a/packages/webext/scripts/zip.mjs b/packages/webext/scripts/zip.mjs new file mode 100644 index 000000000..8ac858098 --- /dev/null +++ b/packages/webext/scripts/zip.mjs @@ -0,0 +1,118 @@ +// @ts-check +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const require = createRequire(import.meta.url) +const archiver = require('archiver') + +const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const rootDir = path.resolve(packageDir, '../..') +const outDir = path.join(rootDir, 'dist') +const outFile = path.join(outDir, 'vite-devtools-chrome.zip') +const include = [ + 'app/background/**', + 'dist/**', + 'icons/**', + 'manifest.json', + 'package.json', +] +const ignore = [ + '**/.DS_Store', + '**/node_modules/**', + '**/*.d.cts', + '**/*.d.mts', + '**/*.d.ts', + '**/*.map', + '**/*.tsbuildinfo', +] + +const manifest = JSON.parse(fs.readFileSync(path.join(packageDir, 'manifest.json'), 'utf8')) +const requiredFiles = collectManifestFiles(manifest) + +fs.rmSync(outFile, { force: true }) +fs.mkdirSync(outDir, { recursive: true }) + +const output = fs.createWriteStream(outFile) +const archive = archiver('zip', { zlib: { level: 9 } }) +const entries = new Set() + +const done = new Promise((resolve, reject) => { + output.on('close', resolve) + output.on('error', reject) + archive.on('entry', entry => entries.add(entry.name)) + archive.on('error', reject) + archive.on('warning', (error) => { + if (error.code === 'ENOENT') + return + reject(error) + }) +}) + +archive.pipe(output) +for (const pattern of include) { + archive.glob(pattern, { + cwd: packageDir, + ignore, + }) +} + +await archive.finalize() +await done + +const size = archive.pointer() +if (size < 1000) + throw new Error(`Zip file is unexpectedly small: ${size} bytes`) + +for (const file of requiredFiles) { + if (!entries.has(file)) + throw new Error(`Manifest file is missing from zip: ${file}`) +} + +console.log(`Created ${path.relative(rootDir, outFile)} (${formatBytes(size)}, ${entries.size} files)`) + +function collectManifestFiles(manifest) { + const files = new Set(['manifest.json']) + + add(files, manifest.devtools_page) + add(files, manifest.background?.service_worker) + addAll(files, manifest.action?.default_icon) + addAll(files, manifest.icons) + + for (const contentScript of manifest.content_scripts ?? []) { + addAll(files, contentScript.js) + addAll(files, contentScript.css) + } + + return files +} + +function add(files, value) { + if (typeof value === 'string') + files.add(value) +} + +function addAll(files, value) { + if (Array.isArray(value)) { + for (const entry of value) + add(files, entry) + return + } + + if (value && typeof value === 'object') { + for (const entry of Object.values(value)) + add(files, entry) + } +} + +function formatBytes(bytes) { + const units = ['B', 'KB', 'MB', 'GB'] + let size = bytes + let unit = 0 + while (size >= 1024 && unit < units.length - 1) { + size /= 1024 + unit += 1 + } + return `${size.toFixed(unit === 0 ? 0 : 2)} ${units[unit]}` +} diff --git a/packages/webext/src/background.ts b/packages/webext/src/background.ts deleted file mode 100644 index fe44b7bd3..000000000 --- a/packages/webext/src/background.ts +++ /dev/null @@ -1 +0,0 @@ -import 'webext-bridge/background' diff --git a/packages/webext/src/devtools-background.ts b/packages/webext/src/devtools-background.ts deleted file mode 100644 index 7693ba1db..000000000 --- a/packages/webext/src/devtools-background.ts +++ /dev/null @@ -1,34 +0,0 @@ -const checkInfo = { - viteDetected: false, - checkCount: 0, -} -const checkViteInterval = window.setInterval(() => { - if (checkInfo.viteDetected || checkInfo.checkCount > 10) { - clearInterval(checkViteInterval) - return - } - - createDevToolsPanel() - checkInfo.checkCount++ -}, 1000) - -function createDevToolsPanel() { - // TODO (hold-off): detect vite env - chrome.devtools.inspectedWindow.eval('true', (detected) => { - if (!detected || checkInfo.viteDetected) { - return - } - - checkInfo.viteDetected = true - clearInterval(checkViteInterval) - - chrome.devtools.panels.create('Vite', 'icons/128.png', 'pages/devtools-panel.html', (panel) => { - panel.onShown.addListener(() => { - // update devtools panel state here - }) - panel.onHidden.addListener(() => { - // update devtools panel state here - }) - }) - }) -} diff --git a/packages/webext/src/devtools-panel.ts b/packages/webext/src/devtools-panel.ts deleted file mode 100644 index 0c14cb55f..000000000 --- a/packages/webext/src/devtools-panel.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { sendMessage } from 'webext-bridge/devtools' -import { ViteDevToolsEvent } from './event' - -function injectScript(scriptName: string, cb: () => void) { - const src = ` - (function() { - var script = document.constructor.prototype.createElement.call(document, 'script'); - script.src = "${scriptName}"; - script.type = "module"; - document.documentElement.appendChild(script); - script.parentNode.removeChild(script); - })() - ` - let timeoutId: number = null! - function execute() { - clearTimeout(timeoutId) - chrome.devtools.inspectedWindow.eval(src, (res, err) => { - if (err) { - // @ts-expect-error skip type check - timeoutId = setTimeout(() => { - execute() - }, 100) - return - } - - cb() - }) - } - execute() -} - -async function getMetadata() { - return await sendMessage(ViteDevToolsEvent.GET_METADATA, {}, 'content-script') -} - -async function connectToDevToolsApp() { - const metadata = await getMetadata() - window.__NUXT__ = {} - window.__NUXT__.config = { public: {}, app: { baseURL: '/', buildAssetsDir: '/_nuxt/', cdnURL: '', metadata } } - // @ts-expect-error skip type check - const appModule = await import('./app/devtools-app.js') - appModule.mountApp() -} - -function init() { - // inject script to window - injectScript(chrome.runtime.getURL('dist/window.js'), async () => { - connectToDevToolsApp() - }) - - chrome.devtools.network.onNavigated.addListener(() => { - // unmount current devtools client - injectScript(chrome.runtime.getURL('dist/window.js'), () => { - // reconnect to new devtools client - }) - }) -} - -init() diff --git a/packages/webext/src/event.ts b/packages/webext/src/event.ts deleted file mode 100644 index 785d296db..000000000 --- a/packages/webext/src/event.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const VITE_DEVTOOLS_NAMESPACE = 'VITE_DEVTOOLS' -export enum ViteDevToolsEvent { - GET_METADATA = 'vite-devtools:get-metadata', -} diff --git a/packages/webext/src/proxy.ts b/packages/webext/src/proxy.ts deleted file mode 100644 index 2b0f0029f..000000000 --- a/packages/webext/src/proxy.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { allowWindowMessaging, onMessage, sendMessage } from 'webext-bridge/content-script' -import { VITE_DEVTOOLS_NAMESPACE, ViteDevToolsEvent } from './event' - -allowWindowMessaging(VITE_DEVTOOLS_NAMESPACE) -onMessage(ViteDevToolsEvent.GET_METADATA, async () => { - return await sendMessage(ViteDevToolsEvent.GET_METADATA, {}, 'window') -}) diff --git a/packages/webext/src/window.ts b/packages/webext/src/window.ts deleted file mode 100644 index 1c9c0c8da..000000000 --- a/packages/webext/src/window.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { onMessage, setNamespace } from 'webext-bridge/window' -import { VITE_DEVTOOLS_NAMESPACE, ViteDevToolsEvent } from './event' - -setNamespace(VITE_DEVTOOLS_NAMESPACE) -onMessage(ViteDevToolsEvent.GET_METADATA, () => { - // TODO (hold-off): get metadata from api endpoint - return { - backend: 'websocket', - websocket: `${location.protocol.replace('http', 'ws')}//${location.hostname}:7812`, - } -}) diff --git a/packages/webext/tsconfig.json b/packages/webext/tsconfig.json index 14381f3d1..666430e14 100644 --- a/packages/webext/tsconfig.json +++ b/packages/webext/tsconfig.json @@ -1,11 +1,17 @@ { "compilerOptions": { "composite": true, + "target": "esnext", + "lib": ["esnext", "dom", "dom.iterable"], "module": "esnext", "moduleResolution": "Bundler", "resolveJsonModule": true, + "types": ["chrome", "vite/client"], "noEmit": true, - "isolatedDeclarations": true, "skipLibCheck": true - } + }, + "include": [ + "./**/*.ts", + "./app/**/*.vue" + ] } diff --git a/packages/webext/tsdown.config.ts b/packages/webext/tsdown.config.ts index a5ee3e8ca..5c190d523 100644 --- a/packages/webext/tsdown.config.ts +++ b/packages/webext/tsdown.config.ts @@ -1,20 +1,17 @@ import { defineConfig } from 'tsdown' -export default defineConfig([{ +export default defineConfig({ entry: [ - 'src/*.ts', - '!src/proxy.ts', + 'app/scripts/*.ts', ], - clean: false, - external: ['./app/devtools-app.js'], - noExternal: [/^webext-bridge\/.*/], -}, { - entry: ['src/proxy.ts'], - clean: false, - format: 'iife', - outputOptions: { - entryFileNames: '[name].js', + tsconfig: './tsconfig.json', + target: 'esnext', + deps: { + alwaysBundle: [ + /^@vitejs\/devtools-kit(?:\/.*)?$/, + /^devframe(?:\/.*)?$/, + ], }, - noExternal: [/^webext-bridge\/.*/], - inlineOnly: false, -}]) + platform: 'browser', + clean: true, +}) diff --git a/packages/webext/uno.config.ts b/packages/webext/uno.config.ts new file mode 100644 index 000000000..162aa8189 --- /dev/null +++ b/packages/webext/uno.config.ts @@ -0,0 +1,42 @@ +import { sharedShortcuts } from '@vitejs/devtools-ui/unocss/shared-shortcuts' +import { theme } from '@vitejs/devtools-ui/unocss/theme' +import { + defineConfig, + presetIcons, + presetWind3, + transformerDirectives, +} from 'unocss' + +export default defineConfig({ + shortcuts: [ + ...sharedShortcuts, + { + // webcomponent-specific z-index + 'z-viewframe': 'z-20', + 'z-viewframe-resizer': 'z-30', + 'z-floating-dock': 'z-50', + 'z-floating-anchor': 'z-[2147483644]', + 'z-floating-tooltip': 'z-[2147483645]', + 'z-command-palette': 'z-[2147483646]', + }, + ], + transformers: [ + transformerDirectives(), + ], + theme: { + colors: theme.colors, + }, + presets: [ + presetWind3({ + dark: 'media', + variablePrefix: 'vdt-', + }), + presetIcons({ + warn: true, + collections: { + logos: () => import('@iconify-json/logos').then(i => i.icons), + ph: () => import('@iconify-json/ph').then(i => i.icons), + }, + }), + ], +}) diff --git a/packages/webext/vite.config.ts b/packages/webext/vite.config.ts new file mode 100644 index 000000000..2d587657f --- /dev/null +++ b/packages/webext/vite.config.ts @@ -0,0 +1,35 @@ +import { fileURLToPath } from 'node:url' +import Vue from '@vitejs/plugin-vue' +import UnoCSS from 'unocss/vite' +import { defineConfig } from 'vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) +const workspaceDir = fileURLToPath(new URL('..', import.meta.url)) + +export default defineConfig({ + base: './', + plugins: [ + Vue(), + UnoCSS(), + ], + server: { + fs: { + allow: [ + workspaceDir, + ], + }, + }, + build: { + outDir: fileURLToPath(new URL('./dist', import.meta.url)), + emptyOutDir: false, + rolldownOptions: { + input: { + 'devtools-panel': fileURLToPath(new URL('./app/panel/devtools-panel.html', import.meta.url)), + }, + }, + }, + resolve: { + dedupe: ['vue'], + }, + root: rootDir, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a19009d28..5862601a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ catalogs: '@vueuse/nuxt': specifier: ^14.4.0 version: 14.4.0 + archiver: + specifier: ^7.0.1 + version: 7.0.1 magic-string: specifier: ^1.1.0 version: 1.1.0 @@ -1313,16 +1316,43 @@ importers: packages/webext: dependencies: + '@vitejs/devtools': + specifier: workspace:* + version: link:../core + '@vitejs/devtools-kit': + specifier: workspace:* + version: link:../kit + devframe: + specifier: catalog:deps + version: 0.8.2(cac@7.0.0)(srvx@0.12.4) webext-bridge: specifier: catalog:deps version: 6.0.1 devDependencies: + '@types/chrome': + specifier: catalog:types + version: 0.2.5 + '@vitejs/plugin-vue': + specifier: catalog:build + version: 6.0.8(vite@8.2.1(@types/node@25.0.3)(@vitejs/devtools@packages+core)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + archiver: + specifier: catalog:build + version: 7.0.1 tsdown: specifier: catalog:build - version: 0.22.14(@vitejs/devtools@0.4.12)(@volar/typescript@2.4.28(typescript@6.0.3))(oxc-resolver@11.24.2)(publint@0.3.21)(tsx@4.23.9)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))(vue-tsc@3.3.9(typescript@6.0.3)) + version: 0.22.14(@vitejs/devtools@packages+core)(@volar/typescript@2.4.28(typescript@6.0.3))(oxc-resolver@11.24.2)(publint@0.3.21)(tsx@4.23.9)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))(vue-tsc@3.3.9(typescript@6.0.3)) tsx: specifier: catalog:build version: 4.23.9 + unocss: + specifier: catalog:build + version: 66.7.5(@unocss/webpack@66.7.5(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1(@types/node@25.0.3)(@vitejs/devtools@packages+core)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0))(webpack@5.104.1(esbuild@0.28.1)))(vite@8.2.1(@types/node@25.0.3)(@vitejs/devtools@packages+core)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0)) + vite: + specifier: ^8.2.1 + version: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@packages+core)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) + vue: + specifier: catalog:deps + version: 3.5.41(typescript@6.0.3) playgrounds/core: dependencies: @@ -1695,11 +1725,6 @@ packages: peerDependencies: devframe: 0.7.15 - '@devframes/hub@0.8.0': - resolution: {integrity: sha512-0eaxezZG+u/n0GKpf9jVlujBzpOvPKeBbvl/5IWD+p/Yv+1Pr5NaTOQBHvWK3B1M96whylnNdErHwoalcPrHJA==} - peerDependencies: - devframe: 0.8.0 - '@devframes/hub@0.8.2': resolution: {integrity: sha512-3SE+aUy+u1LeMClVK0S9FDmfA3j+kvzgyiIEz7gA+bek0chMuVB0A05ZBdLCZ6E/Tq/CKD7svI7L7ntgStCVew==} peerDependencies: @@ -1714,15 +1739,6 @@ packages: '@devframes/hub': optional: true - '@devframes/json-render@0.8.0': - resolution: {integrity: sha512-nKmLTetl3wbaK2BiN42MKH+/kCfPd6r4FEmwM7DJ5f2QoFt7qxnZJLk4hAl3gWkdhrad7ukp3sOfnsTeFJUNkw==} - peerDependencies: - '@devframes/hub': 0.8.0 - devframe: 0.8.0 - peerDependenciesMeta: - '@devframes/hub': - optional: true - '@devframes/json-render@0.8.2': resolution: {integrity: sha512-LpIMMXDZGHsIlrNDWo1PbLenZcYRlxZ4lZ/vR/5j8bEh9FsSSY5Sd6RVPL93lyl4jz+2Sc+S2j4BKKzk9Zi8Fg==} peerDependencies: @@ -1786,16 +1802,6 @@ packages: resolution: {integrity: sha512-kz10Xk1rMrPMhAn3qPUCr9OUqMXZUW2+HzfqO+3+s+6MfO1thbEsTtQoKW6vqdu0akeEud0kgufFGedurMc/Qw==} hasBin: true - '@devframes/plugin-inspect@0.8.0': - resolution: {integrity: sha512-ixtI2cW4HX0Wmz89OF0mSFT2aeUV3IjC118DBdhzQFJurzisy/DmJPnXAb4S+P4z519SnHz4W6UtHNiamDxyxQ==} - hasBin: true - peerDependencies: - devframe: 0.8.0 - vite: ^8.2.1 - peerDependenciesMeta: - vite: - optional: true - '@devframes/plugin-inspect@0.8.2': resolution: {integrity: sha512-wS9Hd/qwFnZkUZNxQhOarsNf616yupkNU42Yr4Kd1Ee8CNj7GdiktlfAmJdJR22b8HMV4G3gTcRQ8EzeSTDFNQ==} hasBin: true @@ -1806,17 +1812,6 @@ packages: vite: optional: true - '@devframes/plugin-messages@0.8.0': - resolution: {integrity: sha512-Z99f64fs+K5XsvtDCqrPRwftYZHe0W2KifED/BvmZQ62AnKnP2Yx+YCBddS96BB+3JWtUhPHlwNdNSAQAnO+Nw==} - hasBin: true - peerDependencies: - '@devframes/hub': 0.8.0 - devframe: 0.8.0 - vite: ^8.2.1 - peerDependenciesMeta: - vite: - optional: true - '@devframes/plugin-messages@0.8.2': resolution: {integrity: sha512-dhOZdENQBd1RWa+SNpn1vEijRaPzzE28YLj9owA2H0Sd5hQXIVQJkVLyTrSGnXtb7KLkNoMAvThD5cItABasDg==} hasBin: true @@ -1838,16 +1833,6 @@ packages: vite: optional: true - '@devframes/plugin-terminals@0.8.0': - resolution: {integrity: sha512-BjOnHuTQuzjJyWa7WvsVYrjh4CCMRLTosBfK3ZgzCcdX1F3aejmRdUIsIuYxJvWPdEedlZGdYFZbWiEVSxb81w==} - hasBin: true - peerDependencies: - devframe: 0.8.0 - vite: ^8.2.1 - peerDependenciesMeta: - vite: - optional: true - '@devframes/plugin-terminals@0.8.2': resolution: {integrity: sha512-Y2kVxnlmTThKoT3Ew3eGlItMEOFWzoK4XUb9oXmRu8rqFDd+yDYW2ZNy/kDwmClTblDFXSn6Q7Enjxo31f/2BA==} hasBin: true @@ -7113,9 +7098,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -8925,9 +8907,6 @@ packages: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - strip-literal@4.0.0: resolution: {integrity: sha512-PaqAvfUZKBwc/SLmNZtHmzK+v19Z4O4eS3cKPeGvbIv/U3pnyEq4Tuw3/4v/FwfM8VQaEawsyCcOQ0P+kpwWWw==} @@ -9281,18 +9260,6 @@ packages: resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} engines: {node: '>=20'} - unimport@6.3.1: - resolution: {integrity: sha512-43BV4iELfhpZ0JNSedMNdBqomAIaJwBrmTqIUYRb8ly3XVQihcRPAVIOSwb23XVCJgtjSf+f82d5Qpdsxqh8iQ==} - engines: {node: '>=18.12.0'} - peerDependencies: - oxc-parser: '*' - rolldown: ^1.2.3 - peerDependenciesMeta: - oxc-parser: - optional: true - rolldown: - optional: true - unimport@6.4.0: resolution: {integrity: sha512-JJOOuNMFq8b4ZPBKwQUxEcba4MplskDzYI1Lvrf8rJfWphZTWvPNXWa493qsPngHUmub89w6C7j+SeLWTE/UIQ==} engines: {node: '>=18.12.0'} @@ -10092,10 +10059,10 @@ snapshots: '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0(supports-color@10.2.2))(supports-color@10.2.2) '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.29.7 '@babel/traverse': 7.29.7(supports-color@10.2.2) - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3(supports-color@10.2.2) @@ -10107,8 +10074,8 @@ snapshots: '@babel/generator@7.29.7': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -10211,7 +10178,7 @@ snapshots: '@babel/helpers@7.28.6': dependencies: '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/parser@7.29.7': dependencies: @@ -10251,17 +10218,17 @@ snapshots: '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@babel/traverse@7.29.7(supports-color@10.2.2)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -10332,22 +10299,11 @@ snapshots: zigpty: 0.2.1 optional: true - '@devframes/hub@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22))': + '@devframes/hub@0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22))': dependencies: '@standard-schema/spec': 1.1.0 destr: 2.0.5 - devframe: 0.8.0(cac@7.0.0)(srvx@0.11.22) - nostics: 1.2.0 - pathe: 2.0.3 - perfect-debounce: 2.1.0 - tinyexec: 1.3.0 - zigpty: 0.2.1 - - '@devframes/hub@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4))': - dependencies: - '@standard-schema/spec': 1.1.0 - destr: 2.0.5 - devframe: 0.8.0(cac@7.0.0)(srvx@0.12.4) + devframe: 0.8.2(cac@7.0.0)(srvx@0.11.22) nostics: 1.2.0 pathe: 2.0.3 perfect-debounce: 2.1.0 @@ -10384,23 +10340,14 @@ snapshots: '@devframes/hub': 0.7.15(devframe@0.7.15(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) optional: true - '@devframes/json-render@0.8.0(@devframes/hub@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22)))(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22))': - dependencies: - '@json-render/core': 0.19.0(zod@4.4.3) - devframe: 0.8.0(cac@7.0.0)(srvx@0.11.22) - nostics: 1.2.0 - zod: 4.4.3 - optionalDependencies: - '@devframes/hub': 0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22)) - - '@devframes/json-render@0.8.0(@devframes/hub@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4)))(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4))': + '@devframes/json-render@0.8.2(@devframes/hub@0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22)))(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22))': dependencies: '@json-render/core': 0.19.0(zod@4.4.3) - devframe: 0.8.0(cac@7.0.0)(srvx@0.12.4) + devframe: 0.8.2(cac@7.0.0)(srvx@0.11.22) nostics: 1.2.0 zod: 4.4.3 optionalDependencies: - '@devframes/hub': 0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4)) + '@devframes/hub': 0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22)) '@devframes/json-render@0.8.2(@devframes/hub@0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.12.4)))(devframe@0.8.2(cac@7.0.0)(srvx@0.12.4))': dependencies: @@ -10487,22 +10434,11 @@ snapshots: - '@modelcontextprotocol/server' - srvx - '@devframes/plugin-inspect@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22))(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1)': + '@devframes/plugin-inspect@0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22))(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1)': dependencies: '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@6.0.3)) cac: 7.0.0 - devframe: 0.8.0(cac@7.0.0)(srvx@0.11.22) - nostics: 1.2.0 - optionalDependencies: - vite: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@0.4.12)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) - transitivePeerDependencies: - - valibot - - '@devframes/plugin-inspect@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4))(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1)': - dependencies: - '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@6.0.3)) - cac: 7.0.0 - devframe: 0.8.0(cac@7.0.0)(srvx@0.12.4) + devframe: 0.8.2(cac@7.0.0)(srvx@0.11.22) nostics: 1.2.0 optionalDependencies: vite: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@0.4.12)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) @@ -10520,20 +10456,11 @@ snapshots: transitivePeerDependencies: - valibot - '@devframes/plugin-messages@0.8.0(@devframes/hub@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22)))(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22))(vite@8.2.1)': - dependencies: - '@devframes/hub': 0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22)) - cac: 7.0.0 - devframe: 0.8.0(cac@7.0.0)(srvx@0.11.22) - nostics: 1.2.0 - optionalDependencies: - vite: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@0.4.12)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) - - '@devframes/plugin-messages@0.8.0(@devframes/hub@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4)))(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4))(vite@8.2.1)': + '@devframes/plugin-messages@0.8.2(@devframes/hub@0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22)))(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22))(vite@8.2.1)': dependencies: - '@devframes/hub': 0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4)) + '@devframes/hub': 0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22)) cac: 7.0.0 - devframe: 0.8.0(cac@7.0.0)(srvx@0.12.4) + devframe: 0.8.2(cac@7.0.0)(srvx@0.11.22) nostics: 1.2.0 optionalDependencies: vite: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@0.4.12)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) @@ -10556,19 +10483,10 @@ snapshots: optionalDependencies: vite: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@0.4.12)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) - '@devframes/plugin-terminals@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22))(vite@8.2.1)': - dependencies: - cac: 7.0.0 - devframe: 0.8.0(cac@7.0.0)(srvx@0.11.22) - nostics: 1.2.0 - zigpty: 0.2.1 - optionalDependencies: - vite: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@0.4.12)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) - - '@devframes/plugin-terminals@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4))(vite@8.2.1)': + '@devframes/plugin-terminals@0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22))(vite@8.2.1)': dependencies: cac: 7.0.0 - devframe: 0.8.0(cac@7.0.0)(srvx@0.12.4) + devframe: 0.8.2(cac@7.0.0)(srvx@0.11.22) nostics: 1.2.0 zigpty: 0.2.1 optionalDependencies: @@ -10822,7 +10740,7 @@ snapshots: ansis: 4.3.1 cac: 7.0.0 chokidar: 5.0.0 - devframe: 0.8.0(cac@7.0.0)(srvx@0.11.22) + devframe: 0.8.2(cac@7.0.0)(srvx@0.11.22) eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2) jiti: 2.7.0 tinyglobby: 0.2.17 @@ -11670,7 +11588,7 @@ snapshots: ufo: 1.6.4 unctx: 3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1(@types/node@25.0.3)(@vitejs/devtools@packages+core)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0))(webpack@5.104.1(esbuild@0.28.1))) untyped: 2.0.0 - verkit: 0.3.1 + verkit: 0.3.2 transitivePeerDependencies: - magic-string - magicast @@ -11701,7 +11619,7 @@ snapshots: ufo: 1.6.4 unctx: 3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1))) untyped: 2.0.0 - verkit: 0.3.1 + verkit: 0.3.2 transitivePeerDependencies: - magic-string - magicast @@ -11713,7 +11631,7 @@ snapshots: dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1))) - '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@0.4.12(cac@7.0.0)(srvx@0.11.22)(vite@8.2.1))(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(vue@3.5.40(typescript@6.0.3))(webpack@5.104.1(esbuild@0.28.1)) + '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@0.4.12(cac@7.0.0)(srvx@0.11.22)(vite@8.2.1))(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(vue@3.5.41(typescript@6.0.3))(webpack@5.104.1(esbuild@0.28.1)) '@vue/shared': 3.5.40 consola: 3.4.2 defu: 6.1.7 @@ -11737,7 +11655,7 @@ snapshots: ufo: 1.6.4 unctx: 3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1))) unstorage: 1.17.5(db0@0.3.4)(idb-keyval@6.3.0)(ioredis@5.10.1(supports-color@10.2.2)) - vue: 3.5.40(typescript@6.0.3) + vue: 3.5.41(typescript@6.0.3) vue-bundle-renderer: 2.3.1 vue-devtools-stub: 0.1.0 optionalDependencies: @@ -11797,7 +11715,7 @@ snapshots: dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1))) - '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@0.4.12(cac@7.0.0)(srvx@0.12.4)(vite@8.2.1))(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(vue@3.5.40(typescript@6.0.3))(webpack@5.104.1(esbuild@0.28.1)) + '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@0.4.12(cac@7.0.0)(srvx@0.12.4)(vite@8.2.1))(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(vue@3.5.41(typescript@6.0.3))(webpack@5.104.1(esbuild@0.28.1)) '@vue/shared': 3.5.40 consola: 3.4.2 defu: 6.1.7 @@ -11821,7 +11739,7 @@ snapshots: ufo: 1.6.4 unctx: 3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1))) unstorage: 1.17.5(db0@0.3.4)(idb-keyval@6.3.0)(ioredis@5.10.1(supports-color@10.2.2)) - vue: 3.5.40(typescript@6.0.3) + vue: 3.5.41(typescript@6.0.3) vue-bundle-renderer: 2.3.1 vue-devtools-stub: 0.1.0 optionalDependencies: @@ -11881,7 +11799,7 @@ snapshots: dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1))) - '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@packages+kit)(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(vue@3.5.40(typescript@6.0.3))(webpack@5.104.1(esbuild@0.28.1)) + '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@packages+kit)(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(vue@3.5.41(typescript@6.0.3))(webpack@5.104.1(esbuild@0.28.1)) '@vue/shared': 3.5.40 consola: 3.4.2 defu: 6.1.7 @@ -11905,7 +11823,7 @@ snapshots: ufo: 1.6.4 unctx: 3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1))) unstorage: 1.17.5(db0@0.3.4)(idb-keyval@6.3.0)(ioredis@5.10.1(supports-color@10.2.2)) - vue: 3.5.40(typescript@6.0.3) + vue: 3.5.41(typescript@6.0.3) vue-bundle-renderer: 2.3.1 vue-devtools-stub: 0.1.0 optionalDependencies: @@ -13682,6 +13600,30 @@ snapshots: - rollup - unloader + '@unhead/vue@3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@0.4.12(cac@7.0.0)(srvx@0.11.22)(vite@8.2.1))(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(vue@3.5.41(typescript@6.0.3))(webpack@5.104.1(esbuild@0.28.1))': + dependencies: + '@unhead/bundler': 3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@0.4.12(cac@7.0.0)(srvx@0.11.22)(vite@8.2.1))(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(unhead@3.3.1(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)))(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) + hookable: 6.1.1 + unhead: 3.3.1(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) + vue: 3.5.41(typescript@6.0.3) + optionalDependencies: + vite: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@0.4.12)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) + webpack: 5.104.1(esbuild@0.28.1) + transitivePeerDependencies: + - '@farmfe/core' + - '@oxc-project/types' + - '@rspack/core' + - '@unhead/cli' + - '@vitejs/devtools-kit' + - bun-types-no-globals + - esbuild + - lightningcss + - oxc-parser + - rolldown + - rollup + - unloader + '@unhead/vue@3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@0.4.12(cac@7.0.0)(srvx@0.12.4)(vite@8.2.1))(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(vue@3.5.40(typescript@6.0.3))(webpack@5.104.1(esbuild@0.28.1))': dependencies: '@unhead/bundler': 3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@0.4.12(cac@7.0.0)(srvx@0.12.4)(vite@8.2.1))(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(unhead@3.3.1(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)))(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) @@ -13706,6 +13648,30 @@ snapshots: - rollup - unloader + '@unhead/vue@3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@0.4.12(cac@7.0.0)(srvx@0.12.4)(vite@8.2.1))(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(vue@3.5.41(typescript@6.0.3))(webpack@5.104.1(esbuild@0.28.1))': + dependencies: + '@unhead/bundler': 3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@0.4.12(cac@7.0.0)(srvx@0.12.4)(vite@8.2.1))(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(unhead@3.3.1(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)))(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) + hookable: 6.1.1 + unhead: 3.3.1(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) + vue: 3.5.41(typescript@6.0.3) + optionalDependencies: + vite: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@0.4.12)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) + webpack: 5.104.1(esbuild@0.28.1) + transitivePeerDependencies: + - '@farmfe/core' + - '@oxc-project/types' + - '@rspack/core' + - '@unhead/cli' + - '@vitejs/devtools-kit' + - bun-types-no-globals + - esbuild + - lightningcss + - oxc-parser + - rolldown + - rollup + - unloader + '@unhead/vue@3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@packages+kit)(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(vue@3.5.40(typescript@6.0.3))(webpack@5.104.1(esbuild@0.28.1))': dependencies: '@unhead/bundler': 3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@packages+kit)(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(unhead@3.3.1(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)))(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) @@ -13730,6 +13696,30 @@ snapshots: - rollup - unloader + '@unhead/vue@3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@packages+kit)(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(vue@3.5.41(typescript@6.0.3))(webpack@5.104.1(esbuild@0.28.1))': + dependencies: + '@unhead/bundler': 3.3.1(@oxc-project/types@0.143.0)(@vitejs/devtools-kit@packages+kit)(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(unhead@3.3.1(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)))(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) + hookable: 6.1.1 + unhead: 3.3.1(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) + vue: 3.5.41(typescript@6.0.3) + optionalDependencies: + vite: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@0.4.12)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) + webpack: 5.104.1(esbuild@0.28.1) + transitivePeerDependencies: + - '@farmfe/core' + - '@oxc-project/types' + - '@rspack/core' + - '@unhead/cli' + - '@vitejs/devtools-kit' + - bun-types-no-globals + - esbuild + - lightningcss + - oxc-parser + - rolldown + - rollup + - unloader + '@unocss/cli@66.7.5': dependencies: '@jridgewell/remapping': 2.3.5 @@ -14047,8 +14037,8 @@ snapshots: dependencies: '@mapbox/node-pre-gyp': 2.0.3(supports-color@10.2.2) '@rollup/pluginutils': 5.3.0(rollup@4.60.2) - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) + acorn: 8.18.0 + acorn-import-attributes: 1.9.5(acorn@8.18.0) async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 @@ -14064,9 +14054,9 @@ snapshots: '@vitejs/devtools-kit@0.4.12(cac@7.0.0)(srvx@0.11.22)(vite@8.2.1)': dependencies: - '@devframes/hub': 0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22)) - '@devframes/json-render': 0.8.0(@devframes/hub@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22)))(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22)) - devframe: 0.8.0(cac@7.0.0)(srvx@0.11.22) + '@devframes/hub': 0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22)) + '@devframes/json-render': 0.8.2(@devframes/hub@0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22)))(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22)) + devframe: 0.8.2(cac@7.0.0)(srvx@0.11.22) local-pkg: 1.2.1 mlly: 1.8.2 nostics: 1.2.0 @@ -14080,9 +14070,9 @@ snapshots: '@vitejs/devtools-kit@0.4.12(cac@7.0.0)(srvx@0.12.4)(vite@8.2.1)': dependencies: - '@devframes/hub': 0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4)) - '@devframes/json-render': 0.8.0(@devframes/hub@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4)))(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4)) - devframe: 0.8.0(cac@7.0.0)(srvx@0.12.4) + '@devframes/hub': 0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.12.4)) + '@devframes/json-render': 0.8.2(@devframes/hub@0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.12.4)))(devframe@0.8.2(cac@7.0.0)(srvx@0.12.4)) + devframe: 0.8.2(cac@7.0.0)(srvx@0.12.4) local-pkg: 1.2.1 mlly: 1.8.2 nostics: 1.2.0 @@ -14132,7 +14122,7 @@ snapshots: '@oxlint-config-inspector/core': 1.1.1(typescript@6.0.3) '@vitejs/devtools-kit': 0.4.12(cac@7.0.0)(srvx@0.11.22)(vite@8.2.1) cac: 7.0.0 - devframe: 0.8.0(cac@7.0.0)(srvx@0.11.22) + devframe: 0.8.2(cac@7.0.0)(srvx@0.11.22) local-pkg: 1.2.1 nostics: 1.2.0 pathe: 2.0.3 @@ -14150,7 +14140,7 @@ snapshots: '@oxlint-config-inspector/core': 1.1.1(typescript@6.0.3) '@vitejs/devtools-kit': 0.4.12(cac@7.0.0)(srvx@0.12.4)(vite@8.2.1) cac: 7.0.0 - devframe: 0.8.0(cac@7.0.0)(srvx@0.12.4) + devframe: 0.8.2(cac@7.0.0)(srvx@0.12.4) local-pkg: 1.2.1 nostics: 1.2.0 pathe: 2.0.3 @@ -14273,14 +14263,14 @@ snapshots: '@vitejs/devtools@0.4.12(@vitejs/devtools-oxc@0.4.12)(@vitejs/devtools-rolldown@0.4.12)(@vitejs/devtools-vite@0.4.12)(@vitejs/devtools-vitest@0.4.12)(crossws@0.4.10(srvx@0.11.22))(srvx@0.11.22)(typescript@6.0.3)(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1)': dependencies: - '@devframes/hub': 0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22)) - '@devframes/plugin-inspect': 0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22))(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1) - '@devframes/plugin-messages': 0.8.0(@devframes/hub@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22)))(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22))(vite@8.2.1) - '@devframes/plugin-terminals': 0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.11.22))(vite@8.2.1) + '@devframes/hub': 0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22)) + '@devframes/plugin-inspect': 0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22))(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1) + '@devframes/plugin-messages': 0.8.2(@devframes/hub@0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22)))(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22))(vite@8.2.1) + '@devframes/plugin-terminals': 0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.11.22))(vite@8.2.1) '@vitejs/devtools-kit': 0.4.12(cac@7.0.0)(srvx@0.11.22)(vite@8.2.1) birpc: 4.0.0 cac: 7.0.0 - devframe: 0.8.0(cac@7.0.0)(srvx@0.11.22) + devframe: 0.8.2(cac@7.0.0)(srvx@0.11.22) h3: 2.0.1-rc.22(crossws@0.4.10(srvx@0.11.22)) local-pkg: 1.2.1 mlly: 1.8.2 @@ -14288,7 +14278,7 @@ snapshots: obug: 2.1.4 pathe: 2.0.3 vite: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@0.4.12)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) - vue: 3.5.40(typescript@6.0.3) + vue: 3.5.41(typescript@6.0.3) optionalDependencies: '@vitejs/devtools-oxc': 0.4.12(srvx@0.11.22)(typescript@6.0.3)(vite@8.2.1) '@vitejs/devtools-rolldown': 0.4.12(cac@7.0.0)(srvx@0.11.22)(vite@8.2.1)(vue@3.5.41(typescript@6.0.3)) @@ -14304,14 +14294,14 @@ snapshots: '@vitejs/devtools@0.4.12(@vitejs/devtools-oxc@0.4.12)(@vitejs/devtools-rolldown@0.4.12)(@vitejs/devtools-vite@0.4.12)(@vitejs/devtools-vitest@0.4.12)(crossws@0.4.10(srvx@0.12.4))(srvx@0.12.4)(typescript@6.0.3)(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1)': dependencies: - '@devframes/hub': 0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4)) - '@devframes/plugin-inspect': 0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4))(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1) - '@devframes/plugin-messages': 0.8.0(@devframes/hub@0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4)))(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4))(vite@8.2.1) - '@devframes/plugin-terminals': 0.8.0(devframe@0.8.0(cac@7.0.0)(srvx@0.12.4))(vite@8.2.1) + '@devframes/hub': 0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.12.4)) + '@devframes/plugin-inspect': 0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.12.4))(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1) + '@devframes/plugin-messages': 0.8.2(@devframes/hub@0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.12.4)))(devframe@0.8.2(cac@7.0.0)(srvx@0.12.4))(vite@8.2.1) + '@devframes/plugin-terminals': 0.8.2(devframe@0.8.2(cac@7.0.0)(srvx@0.12.4))(vite@8.2.1) '@vitejs/devtools-kit': 0.4.12(cac@7.0.0)(srvx@0.12.4)(vite@8.2.1) birpc: 4.0.0 cac: 7.0.0 - devframe: 0.8.0(cac@7.0.0)(srvx@0.12.4) + devframe: 0.8.2(cac@7.0.0)(srvx@0.12.4) h3: 2.0.1-rc.22(crossws@0.4.10(srvx@0.12.4)) local-pkg: 1.2.1 mlly: 1.8.2 @@ -14319,7 +14309,7 @@ snapshots: obug: 2.1.4 pathe: 2.0.3 vite: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@0.4.12)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) - vue: 3.5.40(typescript@6.0.3) + vue: 3.5.41(typescript@6.0.3) optionalDependencies: '@vitejs/devtools-oxc': 0.4.12(srvx@0.12.4)(typescript@6.0.3)(vite@8.2.1) '@vitejs/devtools-rolldown': 0.4.12(cac@7.0.0)(srvx@0.12.4)(vite@8.2.1)(vue@3.5.41(typescript@6.0.3)) @@ -14357,6 +14347,12 @@ snapshots: vite: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@packages+core)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) vue: 3.5.41(typescript@5.9.3) + '@vitejs/plugin-vue@6.0.8(vite@8.2.1(@types/node@25.0.3)(@vitejs/devtools@packages+core)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.2.1(@types/node@25.0.3)(@vitejs/devtools@packages+core)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.9)(yaml@2.9.0) + vue: 3.5.41(typescript@6.0.3) + '@vitejs/plugin-vue@6.0.8(vite@8.2.1)(vue@3.5.40(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.1 @@ -14625,7 +14621,7 @@ snapshots: '@vue/shared': 3.5.41 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.24 + postcss: 8.5.26 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.40': @@ -14987,13 +14983,13 @@ snapshots: dependencies: event-target-shim: 5.0.1 - acorn-import-attributes@1.9.5(acorn@8.16.0): + acorn-import-attributes@1.9.5(acorn@8.18.0): dependencies: - acorn: 8.16.0 + acorn: 8.18.0 - acorn-import-phases@1.0.4(acorn@8.16.0): + acorn-import-phases@1.0.4(acorn@8.18.0): dependencies: - acorn: 8.16.0 + acorn: 8.18.0 acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -15262,7 +15258,7 @@ snapshots: confbox: 0.2.4 defu: 6.1.7 dotenv: 17.4.2 - exsolve: 1.1.0 + exsolve: 1.1.1 giget: 3.3.0 jiti: 2.7.0 ohash: 2.0.11 @@ -15859,13 +15855,13 @@ snapshots: transitivePeerDependencies: - srvx - devframe@0.8.0(cac@7.0.0)(srvx@0.12.4): + devframe@0.8.2(cac@7.0.0)(srvx@0.11.22): dependencies: '@standard-schema/spec': 1.1.0 birpc: 4.0.0 - crossws: 0.4.10(srvx@0.12.4) + crossws: 0.4.10(srvx@0.11.22) destr: 2.0.5 - h3: 2.0.1-rc.26(crossws@0.4.10(srvx@0.12.4)) + h3: 2.0.1-rc.26(crossws@0.4.10(srvx@0.11.22)) mrmime: 2.0.1 nostics: 1.2.0 pathe: 2.0.3 @@ -16942,8 +16938,6 @@ snapshots: js-tokens@4.0.0: {} - js-tokens@9.0.1: {} - js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -17265,8 +17259,8 @@ snapshots: magicast@0.5.4: dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 source-map-js: 1.2.1 make-asynchronous@1.1.0: @@ -17810,7 +17804,7 @@ snapshots: esbuild: 0.28.1 escape-string-regexp: 5.0.0 etag: 1.8.1 - exsolve: 1.1.0 + exsolve: 1.1.1 globby: 16.2.0 gzip-size: 7.0.0 h3: 1.15.11 @@ -17847,7 +17841,7 @@ snapshots: uncrypto: 0.1.3 unctx: 2.5.0 unenv: 2.0.0-rc.24 - unimport: 6.3.1(esbuild@0.28.1)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) + unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) unplugin-utils: 0.3.2 unstorage: 1.17.5(db0@0.3.4)(idb-keyval@6.3.0)(ioredis@5.10.1(supports-color@10.2.2)) untyped: 2.0.0 @@ -17920,7 +17914,7 @@ snapshots: esbuild: 0.28.1 escape-string-regexp: 5.0.0 etag: 1.8.1 - exsolve: 1.1.0 + exsolve: 1.1.1 globby: 16.2.0 gzip-size: 7.0.0 h3: 1.15.11 @@ -17957,7 +17951,7 @@ snapshots: uncrypto: 0.1.3 unctx: 2.5.0 unenv: 2.0.0-rc.24 - unimport: 6.3.1(esbuild@0.28.1)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) + unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) unplugin-utils: 0.3.2 unstorage: 1.17.5(db0@0.3.4)(idb-keyval@6.3.0)(ioredis@5.10.1(supports-color@10.2.2)) untyped: 2.0.0 @@ -19752,10 +19746,6 @@ snapshots: strip-json-comments@5.0.3: {} - strip-literal@3.1.0: - dependencies: - js-tokens: 9.0.1 - strip-literal@4.0.0: dependencies: js-tokens: 10.0.0 @@ -19841,7 +19831,7 @@ snapshots: terser@5.44.1: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.16.0 + acorn: 8.18.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -20064,7 +20054,7 @@ snapshots: unctx@2.5.0: dependencies: - acorn: 8.16.0 + acorn: 8.18.0 estree-walker: 3.0.3 magic-string: 0.30.21 unplugin: 2.3.11 @@ -20112,35 +20102,6 @@ snapshots: unicorn-magic@0.4.0: {} - unimport@6.3.1(esbuild@0.28.1)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)): - dependencies: - acorn: 8.16.0 - escape-string-regexp: 5.0.0 - estree-walker: 3.0.3 - local-pkg: 1.2.1 - magic-string: 0.30.21 - mlly: 1.8.2 - pathe: 2.0.3 - picomatch: 4.0.5 - pkg-types: 2.3.1 - scule: 1.3.0 - strip-literal: 3.1.0 - tinyglobby: 0.2.17 - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)) - unplugin-utils: 0.3.2 - optionalDependencies: - oxc-parser: 0.142.0 - rolldown: 1.2.3 - transitivePeerDependencies: - - '@farmfe/core' - - '@rspack/core' - - bun-types-no-globals - - esbuild - - rollup - - unloader - - vite - - webpack - unimport@6.4.0(esbuild@0.28.1)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.60.2)(vite@8.2.1)(webpack@5.104.1(esbuild@0.28.1)): dependencies: acorn: 8.18.0 @@ -20284,7 +20245,7 @@ snapshots: unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 - acorn: 8.16.0 + acorn: 8.18.0 picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 @@ -20379,7 +20340,7 @@ snapshots: unwasm@0.5.3: dependencies: - exsolve: 1.1.0 + exsolve: 1.1.1 knitwork: 1.3.0 magic-string: 0.30.21 mlly: 1.8.2 @@ -20925,8 +20886,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.16.0 - acorn-import-phases: 1.0.4(acorn@8.16.0) + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) browserslist: 4.28.6 chrome-trace-event: 1.0.4 enhanced-resolve: 5.21.6 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d6de9dbce..9c206e695 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -59,6 +59,7 @@ catalogs: '@unocss/nuxt': *unocss '@vitejs/plugin-vue': ^6.0.8 '@vueuse/nuxt': ^14.4.0 + archiver: ^7.0.1 esbuild: ^0.28.1 magic-string: ^1.1.0 nitropack: ^2.13.4 diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts index c5032edc4..708e81e87 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts @@ -12,6 +12,7 @@ export declare const CLIENT_CONTEXT_KEY: string; // #region Other export { attachDevToolsFrameNav } +export { buildRemoteDevframeUrl } export { CommandsContext } export { connectRemoteDevTools } export { ConnectRemoteDevToolsOptions } @@ -48,8 +49,11 @@ export { DocksPanelContext } export { FRAME_NAV_CHANNEL } export { FRAME_NAV_VERSION } export { parseRemoteConnection } +export { resolveDockIcon } +export { resolveDockUrl } export { RpcClientEvents } export { RpcStreamingClientHost } export { StreamingSubscribeOptions } +export { stripRemoteConnectionFromUrl } export { WhenClauseContext } // #endregion \ No newline at end of file diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.js b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.js index bb324ec1b..aaed301e3 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.js +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.js @@ -12,8 +12,12 @@ export var CLIENT_CONTEXT_KEY /* const */ // #region Other export { attachDevToolsFrameNav } +export { buildRemoteDevframeUrl } export { connectRemoteDevTools } export { FRAME_NAV_CHANNEL } export { FRAME_NAV_VERSION } export { parseRemoteConnection } +export { resolveDockIcon } +export { resolveDockUrl } +export { stripRemoteConnectionFromUrl } // #endregion \ No newline at end of file diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts index a26256ea0..9d3ab959e 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts @@ -135,6 +135,7 @@ export interface TreeProps { // #region Functions export declare function createDockEntryState(_: DevToolsDockEntry, _: Ref): DockEntryState; +export declare function createDocksContext(_: 'embedded' | 'standalone', _: DevToolsRpcClient, _?: Ref): Promise; export declare function DEFAULT_DOCK_PANEL_STORE(): DockPanelStorage; export declare function sharedStateToRef(_: SharedState): ShallowRef; export declare function useDocksEntries(_: DevToolsRpcClient): Promise>; diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.js b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.js index f07f79d53..e2501e877 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.js +++ b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.js @@ -7,6 +7,7 @@ export var DockEmbedded /* const */ // #region Other export { createDockEntryState } +export { createDocksContext } export { DEFAULT_DOCK_PANEL_STORE } export { DockStandalone } export { sharedStateToRef } diff --git a/tsconfig.json b/tsconfig.json index 07aae2601..5da949215 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "references": [ { "path": "./packages/oxc/src/tsconfig.json" }, { "path": "./packages/rolldown/src/tsconfig.json" }, - { "path": "./packages/vite/src/tsconfig.json" } + { "path": "./packages/vite/src/tsconfig.json" }, + { "path": "./packages/webext/tsconfig.json" } ], "files": [] } diff --git a/turbo.json b/turbo.json index 2a79e323f..49540c0bc 100644 --- a/turbo.json +++ b/turbo.json @@ -56,6 +56,9 @@ }, "@vitejs/devtools-webext#build": { "outputLogs": "new-only", + "dependsOn": [ + "^build" + ], "outputs": [ "dist/**" ] From adea41f78b33b8843eb4bfa0a15a027003755bfe Mon Sep 17 00:00:00 2001 From: arlo Date: Tue, 11 Aug 2026 14:58:33 +0800 Subject: [PATCH 2/3] chore: update --- package.json | 2 +- .../state/__tests__/context-cache.test.ts | 23 +++++++++++++++ .../src/client/webcomponents/state/context.ts | 19 ++++++++++--- packages/webext/package.json | 4 ++- packages/webext/scripts/{zip.mjs => zip.ts} | 28 +++++++++++-------- pnpm-lock.yaml | 20 +++++++++++++ pnpm-workspace.yaml | 1 + 7 files changed, 80 insertions(+), 17 deletions(-) rename packages/webext/scripts/{zip.mjs => zip.ts} (76%) diff --git a/package.json b/package.json index 9ec0c8084..be81eba43 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "typecheck": "vue-tsc -b", "watch": "turbo watch build", "zip": "pnpm run zip:webext", - "zip:webext": "turbo run build --filter=@vitejs/devtools-webext && node packages/webext/scripts/zip.mjs" + "zip:webext": "turbo run build --filter=@vitejs/devtools-webext && pnpm --filter @vitejs/devtools-webext run zip" }, "scripts-info": { "actionspack": "Update pinned GitHub Actions versions", diff --git a/packages/core/src/client/webcomponents/state/__tests__/context-cache.test.ts b/packages/core/src/client/webcomponents/state/__tests__/context-cache.test.ts index ec95f2d0f..9a9712a8f 100644 --- a/packages/core/src/client/webcomponents/state/__tests__/context-cache.test.ts +++ b/packages/core/src/client/webcomponents/state/__tests__/context-cache.test.ts @@ -89,4 +89,27 @@ describe('dock state caches', () => { expect(context.docks.entries[0]?.icon) .toBe('http://localhost:5173/__devtools-vite/favicon.svg') }) + + it('resolves server-hosted launcher icons against the RPC server', async () => { + const rpc = createMockRpc([{ + id: 'vitest', + type: 'launcher', + title: 'Vitest', + icon: '/__devtools-vitest/favicon.svg', + launcher: { + title: 'Vitest UI', + description: 'Start Vitest UI.', + icon: '/__devtools-vitest/favicon.svg', + status: 'idle', + }, + }]) + + const context = await createDocksContext('standalone', rpc) + const entry = context.docks.entries[0] + + expect(entry?.icon) + .toBe('http://localhost:5173/__devtools-vitest/favicon.svg') + expect(entry?.type === 'launcher' && entry.launcher.icon) + .toBe('http://localhost:5173/__devtools-vitest/favicon.svg') + }) }) diff --git a/packages/core/src/client/webcomponents/state/context.ts b/packages/core/src/client/webcomponents/state/context.ts index 0e8cfd915..604d4f111 100644 --- a/packages/core/src/client/webcomponents/state/context.ts +++ b/packages/core/src/client/webcomponents/state/context.ts @@ -28,10 +28,21 @@ export async function createDocksContext( } const dockEntries = await useDocksEntries(rpc) - const resolveEntryIcon = (entry: T): T => ({ - ...entry, - icon: resolveDockIcon(entry.icon, rpc.connection), - }) + const resolveEntryIcon = (entry: T): T => { + const resolved = { + ...entry, + icon: resolveDockIcon(entry.icon, rpc.connection), + } + + if (resolved.type === 'launcher' && resolved.launcher.icon) { + resolved.launcher = { + ...resolved.launcher, + icon: resolveDockIcon(resolved.launcher.icon, rpc.connection), + } + } + + return resolved + } // Client-only dock registry (0.7.10 `DocksEntriesContext` API). Docks // registered here live in this page only, merged over the server-provided diff --git a/packages/webext/package.json b/packages/webext/package.json index 5d9b66509..736766cec 100644 --- a/packages/webext/package.json +++ b/packages/webext/package.json @@ -24,7 +24,8 @@ "build:scripts": "tsdown", "build:client": "vite build", "watch": "tsdown --watch && vite build --watch", - "prepack": "pnpm build" + "prepack": "pnpm build", + "zip": "tsx scripts/zip.ts" }, "dependencies": { "@vitejs/devtools": "workspace:*", @@ -33,6 +34,7 @@ "webext-bridge": "catalog:deps" }, "devDependencies": { + "@types/archiver": "catalog:types", "@types/chrome": "catalog:types", "@vitejs/plugin-vue": "catalog:build", "archiver": "catalog:build", diff --git a/packages/webext/scripts/zip.mjs b/packages/webext/scripts/zip.ts similarity index 76% rename from packages/webext/scripts/zip.mjs rename to packages/webext/scripts/zip.ts index 8ac858098..303616ff9 100644 --- a/packages/webext/scripts/zip.mjs +++ b/packages/webext/scripts/zip.ts @@ -1,11 +1,17 @@ -// @ts-check import fs from 'node:fs' -import { createRequire } from 'node:module' import path from 'node:path' import { fileURLToPath } from 'node:url' +import archiver from 'archiver' -const require = createRequire(import.meta.url) -const archiver = require('archiver') +type ManifestFileCollection = string[] | Record + +interface ExtensionManifest { + devtools_page?: string + background?: { service_worker?: string } + action?: { default_icon?: ManifestFileCollection } + icons?: ManifestFileCollection + content_scripts?: Array<{ js?: string[], css?: string[] }> +} const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const rootDir = path.resolve(packageDir, '../..') @@ -28,7 +34,7 @@ const ignore = [ '**/*.tsbuildinfo', ] -const manifest = JSON.parse(fs.readFileSync(path.join(packageDir, 'manifest.json'), 'utf8')) +const manifest = JSON.parse(fs.readFileSync(path.join(packageDir, 'manifest.json'), 'utf8')) as ExtensionManifest const requiredFiles = collectManifestFiles(manifest) fs.rmSync(outFile, { force: true }) @@ -36,9 +42,9 @@ fs.mkdirSync(outDir, { recursive: true }) const output = fs.createWriteStream(outFile) const archive = archiver('zip', { zlib: { level: 9 } }) -const entries = new Set() +const entries = new Set() -const done = new Promise((resolve, reject) => { +const done = new Promise((resolve, reject) => { output.on('close', resolve) output.on('error', reject) archive.on('entry', entry => entries.add(entry.name)) @@ -72,7 +78,7 @@ for (const file of requiredFiles) { console.log(`Created ${path.relative(rootDir, outFile)} (${formatBytes(size)}, ${entries.size} files)`) -function collectManifestFiles(manifest) { +function collectManifestFiles(manifest: ExtensionManifest): Set { const files = new Set(['manifest.json']) add(files, manifest.devtools_page) @@ -88,12 +94,12 @@ function collectManifestFiles(manifest) { return files } -function add(files, value) { +function add(files: Set, value: string | undefined): void { if (typeof value === 'string') files.add(value) } -function addAll(files, value) { +function addAll(files: Set, value: ManifestFileCollection | undefined): void { if (Array.isArray(value)) { for (const entry of value) add(files, entry) @@ -106,7 +112,7 @@ function addAll(files, value) { } } -function formatBytes(bytes) { +function formatBytes(bytes: number): string { const units = ['B', 'KB', 'MB', 'GB'] let size = bytes let unit = 0 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5862601a1..8bf051f4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -403,6 +403,9 @@ catalogs: specifier: ^4.1.10 version: 4.1.10 types: + '@types/archiver': + specifier: ^7.0.0 + version: 7.0.0 '@types/chrome': specifier: ^0.2.5 version: 0.2.5 @@ -1329,6 +1332,9 @@ importers: specifier: catalog:deps version: 6.0.1 devDependencies: + '@types/archiver': + specifier: catalog:types + version: 7.0.0 '@types/chrome': specifier: catalog:types version: 0.2.5 @@ -4112,6 +4118,9 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/archiver@7.0.0': + resolution: {integrity: sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -4312,6 +4321,9 @@ packages: '@types/react@19.2.18': resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + '@types/readdir-glob@1.1.5': + resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} + '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -13157,6 +13169,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/archiver@7.0.0': + dependencies: + '@types/readdir-glob': 1.1.5 + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -13391,6 +13407,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/readdir-glob@1.1.5': + dependencies: + '@types/node': 25.0.3 + '@types/resolve@1.20.2': {} '@types/splitpanes@2.2.6': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9c206e695..6d18f2984 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -207,6 +207,7 @@ catalogs: tsnapi: ^1.2.0 vitest: ^4.1.10 types: + '@types/archiver': ^7.0.0 '@types/chrome': ^0.2.5 '@types/connect': ^3.4.38 '@types/d3': ^7.4.3 From 7e5268be060c46ec71b0fdf71702294b4d477379 Mon Sep 17 00:00:00 2001 From: arlo Date: Wed, 12 Aug 2026 18:21:39 +0800 Subject: [PATCH 3/3] feat(webext): add page detection and popup --- packages/core/src/client/inject/runtime.ts | 4 +- packages/webext/app/popup/App.vue | 118 +++++++++++++++++++ packages/webext/app/popup/main.ts | 8 ++ packages/webext/app/popup/popup.html | 14 +++ packages/webext/app/popup/styles/main.css | 15 +++ packages/webext/app/scripts/background.ts | 128 +++++++++++++++++++++ packages/webext/app/scripts/devtools-bg.ts | 6 +- packages/webext/app/shared/action-icon.ts | 23 ++++ packages/webext/manifest.json | 11 ++ packages/webext/package.json | 2 +- packages/webext/scripts/zip.ts | 3 +- packages/webext/uno.config.ts | 1 + packages/webext/vite.config.ts | 1 + 13 files changed, 329 insertions(+), 5 deletions(-) create mode 100644 packages/webext/app/popup/App.vue create mode 100644 packages/webext/app/popup/main.ts create mode 100644 packages/webext/app/popup/popup.html create mode 100644 packages/webext/app/popup/styles/main.css create mode 100644 packages/webext/app/shared/action-icon.ts diff --git a/packages/core/src/client/inject/runtime.ts b/packages/core/src/client/inject/runtime.ts index 2d4de62ac..614bfad50 100644 --- a/packages/core/src/client/inject/runtime.ts +++ b/packages/core/src/client/inject/runtime.ts @@ -185,12 +185,12 @@ export function startDevTools(initialMode: InjectMode): void { }) if (initialMode === 'normal') { - void connectionReady.then(() => mountDock()) + connectionReady.then(() => mountDock()) } else { // Passive/hidden mode prepares the external-viewer connection eagerly, // while keeping a failed attempt retryable when the overlay is activated. - void connectionReady.catch(() => {}) + connectionReady.catch(() => {}) armPassive(persist) } } diff --git a/packages/webext/app/popup/App.vue b/packages/webext/app/popup/App.vue new file mode 100644 index 000000000..20faafd3f --- /dev/null +++ b/packages/webext/app/popup/App.vue @@ -0,0 +1,118 @@ + + + diff --git a/packages/webext/app/popup/main.ts b/packages/webext/app/popup/main.ts new file mode 100644 index 000000000..92163ac0d --- /dev/null +++ b/packages/webext/app/popup/main.ts @@ -0,0 +1,8 @@ +import { createApp } from 'vue' +import App from './App.vue' + +import '@unocss/reset/tailwind.css' +import './styles/main.css' +import 'uno.css' + +createApp(App).mount('#app') diff --git a/packages/webext/app/popup/popup.html b/packages/webext/app/popup/popup.html new file mode 100644 index 000000000..6c456871b --- /dev/null +++ b/packages/webext/app/popup/popup.html @@ -0,0 +1,14 @@ + + + + + + Vite DevTools + + + +
+ + + + diff --git a/packages/webext/app/popup/styles/main.css b/packages/webext/app/popup/styles/main.css new file mode 100644 index 000000000..a06371fe7 --- /dev/null +++ b/packages/webext/app/popup/styles/main.css @@ -0,0 +1,15 @@ +html { + color-scheme: light dark; +} + +html, +body, +#app { + margin: 0; + min-height: 100%; + padding: 0; +} + +body { + min-width: 320px; +} diff --git a/packages/webext/app/scripts/background.ts b/packages/webext/app/scripts/background.ts index e69de29bb..fadbd7aab 100644 --- a/packages/webext/app/scripts/background.ts +++ b/packages/webext/app/scripts/background.ts @@ -0,0 +1,128 @@ +import { DEVFRAME_CONNECTION_KEY } from 'devframe/constants' +import { setActionIcon } from '../shared/action-icon' + +type DetectionStatus = 'detected' | 'vite-detected' | 'not-detected' | 'restricted' | 'error' + +const DETECTION_TIMEOUT = 10_000 +const DETECTION_INTERVAL = 500 +const RESTRICTED_PROTOCOLS = new Set([ + 'about:', + 'chrome:', + 'chrome-extension:', + 'devtools:', + 'edge:', +]) +const DETECTION_STATUSES = new Set([ + 'detected', + 'vite-detected', + 'not-detected', +]) + +function isRestrictedUrl(url: string | undefined): boolean { + if (!url) + return false + + try { + const parsed = new URL(url) + return RESTRICTED_PROTOCOLS.has(parsed.protocol) + || parsed.hostname === 'chromewebstore.google.com' + || (parsed.hostname === 'chrome.google.com' && parsed.pathname.startsWith('/webstore')) + } + catch { + return true + } +} + +async function detectPage( + connectionKey: string, + timeout: number, + interval: number, +): Promise { + const sleep = (duration: number) => new Promise(resolve => setTimeout(resolve, duration)) + const isViteDevToolsEnabled = () => { + const connection = (globalThis as Record)[connectionKey] + return Boolean( + connection + && typeof connection === 'object' + && typeof (connection as { metaBaseUrl?: unknown }).metaBaseUrl === 'string' + && (connection as { metaBaseUrl: string }).metaBaseUrl, + ) + } + + const viteDetected = Array.from( + document.querySelectorAll('script[type="module"][src]'), + ).some((script) => { + const url = new URL(script.src, document.baseURI) + return url.pathname.endsWith('/@vite/client') + }) + + if (isViteDevToolsEnabled()) + return 'detected' + if (!viteDetected) + return 'not-detected' + + const attempts = Math.ceil(timeout / interval) + for (let attempt = 0; attempt < attempts; attempt++) { + await sleep(interval) + + if (isViteDevToolsEnabled()) + return 'detected' + } + + return 'vite-detected' +} + +async function detectTab( + tab: chrome.tabs.Tab, + timeout = 0, +): Promise { + if (!tab.id) + return 'error' + if (isRestrictedUrl(tab.url)) + return 'restricted' + + try { + const [injection] = await chrome.scripting.executeScript({ + args: [DEVFRAME_CONNECTION_KEY, timeout, DETECTION_INTERVAL], + func: detectPage, + target: { tabId: tab.id }, + world: 'MAIN', + }) + const status = injection?.result + return status && DETECTION_STATUSES.has(status) ? status : 'error' + } + catch { + return isRestrictedUrl(tab.url) ? 'restricted' : 'error' + } +} + +async function updateTabIcon(tab: chrome.tabs.Tab, timeout = 0): Promise { + const status = await detectTab(tab, timeout) + if (tab.id) + await setActionIcon(tab.id, status === 'detected') + return status +} + +chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (changeInfo.status === 'loading') { + setActionIcon(tabId, false) + } + else if (changeInfo.status === 'complete') { + updateTabIcon(tab, DETECTION_TIMEOUT) + } +}) + +chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message?.type !== 'vite-devtools:detect-active-tab') + return + + chrome.tabs.query({ active: true, currentWindow: true }) + .then(async ([tab]) => { + if (!tab) + return 'error' satisfies DetectionStatus + return updateTabIcon(tab) + }) + .then(sendResponse, () => sendResponse('error')) + + return true +}) diff --git a/packages/webext/app/scripts/devtools-bg.ts b/packages/webext/app/scripts/devtools-bg.ts index fbda17aee..1742a00df 100644 --- a/packages/webext/app/scripts/devtools-bg.ts +++ b/packages/webext/app/scripts/devtools-bg.ts @@ -1,4 +1,5 @@ import { DEVFRAME_CONNECTION_KEY } from 'devframe/constants' +import { setActionIcon } from '../shared/action-icon' const META_CHECK_INTERVAL = 1000 const META_CHECK_ATTEMPTS = 10 @@ -38,8 +39,10 @@ async function checkPanel() { for (let attempt = 0; attempt < META_CHECK_ATTEMPTS; attempt++) { if (await checkConnectionMeta()) { - if (currentCheckId === checkId) + if (currentCheckId === checkId) { + setActionIcon(chrome.devtools.inspectedWindow.tabId, true) createPanel() + } return } @@ -52,6 +55,7 @@ async function checkPanel() { } chrome.devtools.network.onNavigated.addListener(() => { + setActionIcon(chrome.devtools.inspectedWindow.tabId, false) checkPanel() }) diff --git a/packages/webext/app/shared/action-icon.ts b/packages/webext/app/shared/action-icon.ts new file mode 100644 index 000000000..36436d42b --- /dev/null +++ b/packages/webext/app/shared/action-icon.ts @@ -0,0 +1,23 @@ +const ACTIVE_ICONS = { + 16: '/icons/16.png', + 48: '/icons/48.png', + 128: '/icons/128.png', +} + +const INACTIVE_ICONS = { + 16: '/icons/16-inactive.png', + 48: '/icons/48-inactive.png', + 128: '/icons/128-inactive.png', +} + +export async function setActionIcon(tabId: number, active: boolean): Promise { + try { + await chrome.action.setIcon({ + tabId, + path: active ? ACTIVE_ICONS : INACTIVE_ICONS, + }) + } + catch { + // Ignore expected tab-close races. + } +} diff --git a/packages/webext/manifest.json b/packages/webext/manifest.json index b85b8e018..c925776a4 100644 --- a/packages/webext/manifest.json +++ b/packages/webext/manifest.json @@ -1,12 +1,17 @@ { "action": { "default_title": "Vite DevTools", + "default_popup": "dist/app/popup/popup.html", "default_icon": { "16": "icons/16-inactive.png", "48": "icons/48-inactive.png", "128": "icons/128-inactive.png" } }, + "background": { + "service_worker": "dist/background.js", + "type": "module" + }, "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" }, @@ -17,8 +22,14 @@ "48": "icons/48.png", "128": "icons/128.png" }, + "host_permissions": [ + "" + ], "manifest_version": 3, "name": "Vite DevTools", + "permissions": [ + "scripting" + ], "version": "0.0.0", "version_name": "0.0.0" } diff --git a/packages/webext/package.json b/packages/webext/package.json index 736766cec..a454f28af 100644 --- a/packages/webext/package.json +++ b/packages/webext/package.json @@ -23,7 +23,7 @@ "build": "pnpm run build:scripts && pnpm run build:client", "build:scripts": "tsdown", "build:client": "vite build", - "watch": "tsdown --watch && vite build --watch", + "watch": "turbo watch build --filter=@vitejs/devtools-webext", "prepack": "pnpm build", "zip": "tsx scripts/zip.ts" }, diff --git a/packages/webext/scripts/zip.ts b/packages/webext/scripts/zip.ts index 303616ff9..8fdcb229f 100644 --- a/packages/webext/scripts/zip.ts +++ b/packages/webext/scripts/zip.ts @@ -8,7 +8,7 @@ type ManifestFileCollection = string[] | Record interface ExtensionManifest { devtools_page?: string background?: { service_worker?: string } - action?: { default_icon?: ManifestFileCollection } + action?: { default_icon?: ManifestFileCollection, default_popup?: string } icons?: ManifestFileCollection content_scripts?: Array<{ js?: string[], css?: string[] }> } @@ -83,6 +83,7 @@ function collectManifestFiles(manifest: ExtensionManifest): Set { add(files, manifest.devtools_page) add(files, manifest.background?.service_worker) + add(files, manifest.action?.default_popup) addAll(files, manifest.action?.default_icon) addAll(files, manifest.icons) diff --git a/packages/webext/uno.config.ts b/packages/webext/uno.config.ts index 162aa8189..3f2d676b7 100644 --- a/packages/webext/uno.config.ts +++ b/packages/webext/uno.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ shortcuts: [ ...sharedShortcuts, { + 'status-ready': 'bg-green-500/10 color-green-600 dark:color-green-400', // webcomponent-specific z-index 'z-viewframe': 'z-20', 'z-viewframe-resizer': 'z-30', diff --git a/packages/webext/vite.config.ts b/packages/webext/vite.config.ts index 2d587657f..3c54089bc 100644 --- a/packages/webext/vite.config.ts +++ b/packages/webext/vite.config.ts @@ -25,6 +25,7 @@ export default defineConfig({ rolldownOptions: { input: { 'devtools-panel': fileURLToPath(new URL('./app/panel/devtools-panel.html', import.meta.url)), + 'popup': fileURLToPath(new URL('./app/popup/popup.html', import.meta.url)), }, }, },