diff --git a/.changeset/large-donuts-perform.md b/.changeset/large-donuts-perform.md new file mode 100644 index 00000000..f9f21ea3 --- /dev/null +++ b/.changeset/large-donuts-perform.md @@ -0,0 +1,5 @@ +--- +'@solid-devtools/extension': minor +--- + +Support firefox (minor changes to background script) diff --git a/.changeset/young-baboons-compare.md b/.changeset/young-baboons-compare.md new file mode 100644 index 00000000..03894d72 --- /dev/null +++ b/.changeset/young-baboons-compare.md @@ -0,0 +1,6 @@ +--- +'@solid-devtools/frontend': patch +'@solid-devtools/overlay': patch +--- + +Add missing dependencies diff --git a/.gitignore b/.gitignore index 057f7915..e8015bc7 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,6 @@ Thumbs.db # chrome extension private key file *.pem -packages/extension/dist.zip **/vite.config.ts.timestamp-*.* /test-results/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c6274134..0298fba7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,7 +74,7 @@ This project uses [changesets](https://github.com/changesets/changesets) to mana ![image](https://user-images.githubusercontent.com/24491503/191084587-e53b1743-39ac-40e0-b3a6-cf6bcaca9d5d.png) -4. Click on the "Load unpacked" button and select the `packages/extension/dist` folder +4. Click on the "Load unpacked" button and select the `packages/extension/dist/chrome` directory (or `packages/extension/dist/firefox` if you are using firefox) ![image](https://user-images.githubusercontent.com/24491503/191084770-84577eb0-1c90-44a7-afa2-a2d03f728a66.png) diff --git a/configs/tsconfig.base.json b/configs/tsconfig.base.json index acb51c0b..5752eb17 100644 --- a/configs/tsconfig.base.json +++ b/configs/tsconfig.base.json @@ -1,17 +1,16 @@ { - "compilerOptions": { - "strict": true, - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true, - "esModuleInterop": true, - "noEmit": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "noPropertyAccessFromIndexSignature": true, - "noUncheckedIndexedAccess": true, - "types": ["@total-typescript/ts-reset"] - } + "compilerOptions": { + "strict": true, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + "types": ["@total-typescript/ts-reset"] + } } diff --git a/e2e/fixtures.ts b/e2e/fixtures.ts index 4d35715a..9592d1f1 100644 --- a/e2e/fixtures.ts +++ b/e2e/fixtures.ts @@ -18,7 +18,7 @@ export const test = pw.test.extend<{ return } - const path_to_extension = path.resolve(__dirname, '../packages/extension/dist/') + const path_to_extension = path.resolve(__dirname, '../packages/extension/dist/chrome') const context = await pw.chromium.launchPersistentContext('', { args: [ '--headless=new', diff --git a/playwright.config.ts b/e2e/playwright.config.ts similarity index 98% rename from playwright.config.ts rename to e2e/playwright.config.ts index 20c6f64f..8d8d17ac 100644 --- a/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -6,7 +6,7 @@ const is_ci = !!process.env['CI'] * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './e2e', + testDir: '.', /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ diff --git a/examples/sandbox/vite.config.ts b/examples/sandbox/vite.config.ts index 95f4be9f..b78b2e94 100644 --- a/examples/sandbox/vite.config.ts +++ b/examples/sandbox/vite.config.ts @@ -5,10 +5,10 @@ import { defineConfig } from 'vite' import Inspect from 'vite-plugin-inspect' import solid from 'vite-plugin-solid' -const usingExtension = process.env['EXT'] === 'true' || process.env['EXT'] === '1' +const is_ext = process.env['EXT'] === 'true' || process.env['EXT'] === '1' export default defineConfig(mode => { - const isBuild = mode.command === 'build' + const is_build = mode.command === 'build' return { plugins: [ @@ -27,8 +27,8 @@ export default defineConfig(mode => { Inspect(), ], define: { - 'process.env.EXT': JSON.stringify(usingExtension), - 'process.env.BUILD': JSON.stringify(isBuild), + 'process.env.EXT': JSON.stringify(is_ext), + 'process.env.BUILD': JSON.stringify(is_build), }, mode: 'development', build: { diff --git a/package.json b/package.json index f3654b69..e0203800 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ "build": "turbo run build --filter=./packages/*", "----------------------TEST----------------------": "", "test:unit": "turbo run test --filter=./packages/*", - "test:types": "turbo run typecheck --filter=./packages/*", + "test:types": "turbo run test:types --filter=./packages/*", "test:lint": "eslint --ignore-path .gitignore --max-warnings 0 packages/**/*.{js,ts,tsx,jsx} --rule \"@typescript-eslint/no-unnecessary-type-assertion: off\"", - "test:e2e": "cross-env PW_CHROMIUM_ATTACH_TO_OTHER=1 playwright test -c playwright.config.ts", + "test:e2e": "cross-env PW_CHROMIUM_ATTACH_TO_OTHER=1 playwright test -c e2e/playwright.config.ts", "----------------------UTILS----------------------": "", "build-test:packages": "turbo run build test:unit test:types --filter=./packages/*", "build-test": "pnpm run build-test:packages && pnpm run test:lint && pnpm run test:e2e", @@ -28,8 +28,8 @@ "publish": "pnpm run build && pnpm run changeset publish" }, "devDependencies": { + "@solid-devtools/theme": "file:packages/theme", "@changesets/cli": "^2.26.2", - "@nothing-but/utils": "~0.11.1", "@playwright/test": "^1.39.0", "@total-typescript/ts-reset": "^0.5.1", "@types/node": "^20.8.10", @@ -48,6 +48,7 @@ "solid-js": "^1.8.5", "tsup": "^7.2.0", "tsup-preset-solid": "^2.1.0", + "tsx": "^3.14.0", "turbo": "^1.10.16", "typescript": "^5.2.2", "unocss": "^0.57.1", diff --git a/packages/debugger/src/main/types.ts b/packages/debugger/src/main/types.ts index 959e40d6..c225521c 100644 --- a/packages/debugger/src/main/types.ts +++ b/packages/debugger/src/main/types.ts @@ -74,14 +74,15 @@ export const getValueItemId = ( export type ValueUpdateListener = (newValue: unknown, oldValue: unknown) => void export namespace Solid { - export type OwnerBase = import('solid-js/types/reactive/signal').Owner - export type SourceMapValue = import('solid-js/types/reactive/signal').SourceMapValue - export type Signal = import('solid-js/types/reactive/signal').SignalState - export type Computation = import('solid-js/types/reactive/signal').Computation - export type Memo = import('solid-js/types/reactive/signal').Memo - export type RootFunction = import('solid-js/types/reactive/signal').RootFunction - export type EffectFunction = import('solid-js/types/reactive/signal').EffectFunction - export type Component = import('solid-js/types/reactive/signal').DevComponent<{ + export type OwnerBase = import('solid-js').Owner + export type SourceMapValue = import('solid-js/types/reactive/signal.d.ts').SourceMapValue + export type Signal = import('solid-js/types/reactive/signal.d.ts').SignalState + export type Computation = import('solid-js/types/reactive/signal.d.ts').Computation + export type Memo = import('solid-js/types/reactive/signal.d.ts').Memo + export type RootFunction = import('solid-js/types/reactive/signal.d.ts').RootFunction + export type EffectFunction = + import('solid-js/types/reactive/signal.d.ts').EffectFunction + export type Component = import('solid-js/types/reactive/signal.d.ts').DevComponent<{ [key: string]: unknown }> @@ -109,12 +110,12 @@ export namespace Solid { // export type StoreNode = import('solid-js/store').StoreNode - export type NotWrappable = import('solid-js/store/types/store').NotWrappable - export type OnStoreNodeUpdate = import('solid-js/store/types/store').OnStoreNodeUpdate + export type NotWrappable = import('solid-js/store').NotWrappable + export type OnStoreNodeUpdate = import('solid-js/store/types/store.d.ts').OnStoreNodeUpdate export type Store = SourceMapValue & { value: StoreNode } } -declare module 'solid-js/types/reactive/signal' { +declare module 'solid-js/types/reactive/signal.d.ts' { interface Owner { sdtType?: NodeType sdtSubRoots?: Solid.Owner[] | null diff --git a/packages/extension/background/background.ts b/packages/extension/background/background.ts index faf6062c..abf9d0fd 100644 --- a/packages/extension/background/background.ts +++ b/packages/extension/background/background.ts @@ -7,25 +7,11 @@ It has to coordinate the communication between the different scripts based on th */ import { error, log } from '@solid-devtools/shared/utils' -import { - ConnectionName, - DetectionState, - ForwardPayload, - OnMessageFn, - PostMessageFn, - Versions, - createPortMessanger, - isForwardMessage, - once, -} from '../src/bridge' -import { EventBus } from '../src/event-bus' -import icons from '../src/icons' +import * as bridge from '../shared/bridge' +import { icons } from '../shared/icons' log('Background script working.') -let activeTabId: number = -1 -chrome.tabs.onActivated.addListener(({ tabId }) => (activeTabId = tabId)) - type TabDataConfig = { toContent: TabData['toContent'] fromContent: TabData['fromContent'] @@ -33,18 +19,27 @@ type TabDataConfig = { forwardToClient: TabData['forwardToClient'] } +type PostMessanger = { post: bridge.PostMessageFn; on: bridge.OnMessageFn } + +class EventBus extends Set<(payload: T) => void> { + emit(..._: void extends T ? [payload?: T] : [payload: T]): void + emit(payload?: any) { + for (const cb of this) cb(payload) + } +} + class TabData { public connected = true private disconnectBus = new EventBus() private connectListeners = new Set< - (toContent: PostMessageFn, fromContent: OnMessageFn) => void + (toContent: bridge.PostMessageFn, fromContent: bridge.OnMessageFn) => void >() - private toContent: PostMessageFn - private fromContent: OnMessageFn - public forwardToDevtools: (fn: (message: ForwardPayload) => void) => void - public forwardToClient: (message: ForwardPayload) => void + private toContent: bridge.PostMessageFn + private fromContent: bridge.OnMessageFn + public forwardToDevtools: (fn: (message: bridge.ForwardPayload) => void) => void + public forwardToClient: (message: bridge.ForwardPayload) => void constructor( public tabId: number, @@ -56,12 +51,16 @@ class TabData { this.forwardToClient = config.forwardToClient } - onContentScriptConnect( - fn: (toContent: PostMessageFn, fromContent: OnMessageFn) => void, - ): VoidFunction { - if (this.connected) fn(this.toContent.bind(this), this.fromContent.bind(this)) - this.connectListeners.add(fn) - return () => this.connectListeners.delete(fn) + untilContentScriptConnect(): Promise { + return new Promise(resolve => { + if (this.connected) { + resolve({ post: this.toContent.bind(this), on: this.fromContent.bind(this) }) + } else { + this.connectListeners.add((toContent, fromContent) => { + resolve({ post: toContent, on: fromContent }) + }) + } + }) } reconnected(config: TabDataConfig) { @@ -88,145 +87,212 @@ class TabData { this.forwardToDevtools = () => {} } - #versions: Versions | undefined - #versionsBus = new EventBus() - onVersions(fn: (versions: Versions) => void) { + #versions: bridge.Versions | undefined + #versionsBus = new EventBus() + onVersions(fn: (versions: bridge.Versions) => void) { if (this.#versions) fn(this.#versions) else this.#versionsBus.add(fn) } - setVersions(versions: Versions) { + setVersions(versions: bridge.Versions) { this.#versions = versions this.#versionsBus.emit(versions) this.#versionsBus.clear() } - #detected: DetectionState = { + #detected: bridge.DetectionState = { Solid: false, SolidDev: false, Devtools: false, } - #detectedListeners = new EventBus() - onDetected(fn: (state: DetectionState) => void) { + #detectedListeners = new EventBus() + onDetected(fn: (state: bridge.DetectionState) => void) { fn(this.#detected) this.#detectedListeners.add(fn) } - detected(state: DetectionState) { + detected(state: bridge.DetectionState) { this.#detected = state this.#detectedListeners.emit(state) } } -const tabDataMap = new Map() +const ACTIVE_TAB_QUERY = { active: true, currentWindow: true } as const +const queryActiveTabId = async (): Promise => { + try { + const tabs = await chrome.tabs.query(ACTIVE_TAB_QUERY) + if (tabs.length === 0) return new Error('No active tab') + const tab = tabs[0]! + if (!tab.id) return new Error('Active tab has no id') + return tab.id + } catch (e) { + return e instanceof Error ? e : new Error('Unknown error') + } +} -// for reconnecting after page reload -let lastDisconnectedTabData: TabData | undefined -let lastDisconnectedTabId: number | undefined +let last_active_tab_id = -1 +if (import.meta.env.BROWSER === 'chrome') { + chrome.tabs.onActivated.addListener(info => { + last_active_tab_id = info.tabId + }) +} -function handleContentScriptConnection(port: chrome.runtime.Port, tabId: number) { - const { onPortMessage: fromContent, postPortMessage: toContent } = createPortMessanger(port) +const tab_data_map = new Map() - let forwardHandler: ((message: ForwardPayload) => void) | undefined - let data: TabData +const getActiveTabData = async (): Promise => { + let active_tab_id = last_active_tab_id - const config: TabDataConfig = { - toContent, - fromContent, - forwardToDevtools: fn => (forwardHandler = fn), - forwardToClient: message => port.postMessage(message), + /* + quering for active data works on chrome too + but it breaks e2e tests for some reason + */ + if (import.meta.env.BROWSER === 'firefox') { + const result = await queryActiveTabId() + if (result instanceof Error) return result + active_tab_id = result } - // Page was reloaded, so we need to reinitialize the tab data - if (tabId === lastDisconnectedTabId) { - data = lastDisconnectedTabData! - data.reconnected(config) - } - // A fresh page - else { - data = new TabData(tabId, config) - } + const data = tab_data_map.get(active_tab_id) + if (!data) return new Error(`No data for active tab "${active_tab_id}"`) - lastDisconnectedTabId = undefined - lastDisconnectedTabId = undefined - tabDataMap.set(tabId, data) + return data +} - // "Versions" from content-script - once(fromContent, 'Versions', v => { - data.setVersions(v) +// for reconnecting after page reload +let last_disconnected_tab_data: TabData | undefined +let last_disconnected_tab_id: number | undefined - // Change the popup icon to indicate that Solid is present on the page - chrome.action.setIcon({ tabId, path: icons.normal }) - }) +chrome.runtime.onConnect.addListener(async port => { + switch (port.name) { + case bridge.ConnectionName.Content: { + const tab_id = port.sender?.tab?.id + if (typeof tab_id !== 'number') break + + const content_messanger = bridge.createPortMessanger(port) + + let forwardHandler: ((message: bridge.ForwardPayload) => void) | undefined + let data: TabData + + const config: TabDataConfig = { + toContent: content_messanger.postPortMessage, + fromContent: content_messanger.onPortMessage, + forwardToDevtools: fn => (forwardHandler = fn), + forwardToClient: message => port.postMessage(message), + } + + // Page was reloaded, so we need to reinitialize the tab data + if (tab_id === last_disconnected_tab_id) { + data = last_disconnected_tab_data! + data.reconnected(config) + } + // A fresh page + else { + data = new TabData(tab_id, config) + } + + last_disconnected_tab_id = undefined + last_disconnected_tab_id = undefined + tab_data_map.set(tab_id, data) + + // "Versions" from content-script + bridge.once(content_messanger.onPortMessage, 'Versions', v => { + data.setVersions(v) + + // Change the popup icon to indicate that Solid is present on the page + chrome.action.setIcon({ tabId: tab_id, path: icons.normal }) + }) - // "DetectSolid" from content-script (realWorld) - fromContent('Detected', state => data.detected(state)) + // "DetectSolid" from content-script (realWorld) + content_messanger.onPortMessage('Detected', state => data.detected(state)) - port.onDisconnect.addListener(() => { - data.disconnected() - tabDataMap.delete(tabId) - lastDisconnectedTabData = data - lastDisconnectedTabId = tabId - }) + port.onDisconnect.addListener(() => { + data.disconnected() + tab_data_map.delete(tab_id) + last_disconnected_tab_data = data + last_disconnected_tab_id = tab_id + }) - port.onMessage.addListener((message: ForwardPayload | any) => { - // HANDLE FORWARDED MESSAGES FROM CLIENT (content-script) - forwardHandler && isForwardMessage(message) && forwardHandler(message) - }) -} + port.onMessage.addListener((message: bridge.ForwardPayload | any) => { + // HANDLE FORWARDED MESSAGES FROM CLIENT (content-script) + forwardHandler && bridge.isForwardMessage(message) && forwardHandler(message) + }) -function withTabData( - port: chrome.runtime.Port, - fn: (data: TabData, m: ReturnType) => void, -): void { - const data = tabDataMap.get(activeTabId) - if (!data) return error('No data for active tab', activeTabId) - const m = createPortMessanger(port) - fn(data, m) -} + break + } + + case bridge.ConnectionName.Devtools: { + const data = await getActiveTabData() + if (data instanceof Error) { + error(data) + break + } + const devtools_messanger = bridge.createPortMessanger(port) + + const content_messanger = await data.untilContentScriptConnect() + + // "Versions" means the devtools client is present + data.onVersions(v => devtools_messanger.postPortMessage('Versions', v)) + + port.onDisconnect.addListener(() => content_messanger.post('DevtoolsClosed')) -chrome.runtime.onConnect.addListener(port => { - switch (port.name) { - case ConnectionName.Content: - port.sender?.tab?.id && handleContentScriptConnection(port, port.sender.tab.id) break + } + + case bridge.ConnectionName.Panel: { + const data = await getActiveTabData() + if (data instanceof Error) { + error(data) + break + } + const panel_messanger = bridge.createPortMessanger(port) + + const content_messanger = await data.untilContentScriptConnect() + + data.onVersions(v => { + panel_messanger.postPortMessage('Versions', v) + // notify the content script that the devtools panel is ready + content_messanger.post('DevtoolsOpened') + }) - case ConnectionName.Devtools: - withTabData(port, (data, { postPortMessage: toDevtools }) => { - data.onContentScriptConnect(toContent => { - // "Versions" means the devtools client is present - data.onVersions(v => toDevtools('Versions', v)) + content_messanger.on('ResetPanel', () => { + panel_messanger.postPortMessage('ResetPanel') + }) + data.onContentScriptDisconnect(() => { + panel_messanger.postPortMessage('ResetPanel') + }) - port.onDisconnect.addListener(() => toContent('DevtoolsClosed')) - }) + /* Force debugger to send state when panel conects */ + data.forwardToClient({ + name: 'ResetState', + details: undefined, + forwarding: true, // TODO: this shouldn't be a "forward", but not sure how to typesafe send a post to debugger from here }) - break - case ConnectionName.Panel: - withTabData(port, (data, { postPortMessage: toPanel, onForwardMessage }) => { - data.onContentScriptConnect((toContent, fromContent) => { - data.onVersions(v => { - toPanel('Versions', v) - // notify the content script that the devtools panel is ready - toContent('DevtoolsOpened') - }) - - fromContent('ResetPanel', () => toPanel('ResetPanel')) - data.onContentScriptDisconnect(() => toPanel('ResetPanel')) - - // FORWARD MESSAGES FROM and TO CLIENT - data.forwardToDevtools(message => { - // console.log('Forwarding to panel', message) - port.postMessage(message) - }) - onForwardMessage(message => data.forwardToClient(message)) - }) + // FORWARD MESSAGES FROM and TO CLIENT + data.forwardToDevtools(message => { + port.postMessage(message) + }) + panel_messanger.onForwardMessage(message => { + data.forwardToClient(message) }) - break - case ConnectionName.Popup: - withTabData(port, (data, { postPortMessage: toPopup }) => { - data.onVersions(v => toPopup('Versions', v)) - data.onDetected(state => toPopup('Detected', state)) + break + } + + case bridge.ConnectionName.Popup: { + const data = await getActiveTabData() + if (data instanceof Error) { + error(data) + break + } + const popup_messanger = bridge.createPortMessanger(port) + + data.onVersions(v => { + popup_messanger.postPortMessage('Versions', v) }) + data.onDetected(state => { + popup_messanger.postPortMessage('Detected', state) + }) + break + } } }) diff --git a/packages/extension/build.ts b/packages/extension/build.ts new file mode 100644 index 00000000..cd672ea1 --- /dev/null +++ b/packages/extension/build.ts @@ -0,0 +1,41 @@ +import child_process from 'node:child_process' +import fs from 'node:fs' +import * as vite from 'vite' + +const cwd = process.cwd() +const args = process.argv.slice(2) + +type Browser = 'chrome' | 'firefox' + +/* +Parse args +*/ +const browsers: Browser[] = [] +for (const arg of args) { + if (!arg.startsWith('--browser=')) continue + + const browser = arg.slice('--browser='.length) + if (browser !== 'chrome' && browser !== 'firefox') { + throw new Error('browser arg must be "chrome" or "firefox", was ' + browser) + } + browsers.push(browser) +} +if (browsers.length === 0) { + throw new Error('No browsers specified') +} + +/* +Build and zip +*/ +const dist = `${cwd}/dist` + +for (const browser of browsers) { + const dist_dir = `${dist}/${browser}` + const dist_zip = `${dist}/${browser}.zip` + + process.env['BROWSER'] = browser + await vite.build() + + if (fs.existsSync(dist_zip)) fs.rmSync(dist_zip) + child_process.exec(`cd ${dist_dir} && zip -r ${dist_zip} .`) +} diff --git a/packages/extension/content/content.ts b/packages/extension/content/content.ts index 7fb1a670..a9943b20 100644 --- a/packages/extension/content/content.ts +++ b/packages/extension/content/content.ts @@ -21,7 +21,7 @@ import { makeMessageListener, makePostMessage, startListeningWindowMessages, -} from '../src/bridge' +} from '../shared/bridge' import.meta.env.DEV && log('Content-Script working.') diff --git a/packages/extension/content/debugger.ts b/packages/extension/content/debugger.ts index d80994ec..f6c80ac4 100644 --- a/packages/extension/content/debugger.ts +++ b/packages/extension/content/debugger.ts @@ -8,7 +8,11 @@ Debugger Client injected into the inspected page import { useDebugger } from '@solid-devtools/debugger' import { Debugger } from '@solid-devtools/debugger/types' import { log, warn } from '@solid-devtools/shared/utils' -import { makeMessageListener, makePostMessage, startListeningWindowMessages } from '../src/bridge' +import { + makeMessageListener, + makePostMessage, + startListeningWindowMessages, +} from '../shared/bridge' import.meta.env.DEV && log('Debugger-Client loaded') diff --git a/packages/extension/content/detector.ts b/packages/extension/content/detector.ts index dd990bf0..f0044603 100644 --- a/packages/extension/content/detector.ts +++ b/packages/extension/content/detector.ts @@ -7,7 +7,7 @@ and notify the content script import '@solid-devtools/debugger/types' import { detectSolid, onSolidDevDetect, onSolidDevtoolsDetect } from '@solid-devtools/shared/detect' -import { DETECT_MESSAGE, DetectEvent, DetectionState } from '../src/bridge' +import { DETECT_MESSAGE, DetectEvent, DetectionState } from '../shared/bridge' const state: DetectionState = { Solid: false, diff --git a/packages/extension/devtools/devtools.html b/packages/extension/devtools/devtools.html index cb9b3e3d..52814b26 100644 --- a/packages/extension/devtools/devtools.html +++ b/packages/extension/devtools/devtools.html @@ -1,12 +1,10 @@ - + + + + - - - - - - - - - \ No newline at end of file + + + + diff --git a/packages/extension/devtools/devtools.ts b/packages/extension/devtools/devtools.ts index dae4c89e..93b893bd 100644 --- a/packages/extension/devtools/devtools.ts +++ b/packages/extension/devtools/devtools.ts @@ -8,8 +8,8 @@ It connects to the background script. */ import { error, log } from '@solid-devtools/shared/utils' -import { ConnectionName, createPortMessanger, once } from '../src/bridge' -import icons from '../src/icons' +import { ConnectionName, createPortMessanger, once } from '../shared/bridge' +import { icons } from '../shared/icons' log('Devtools-Script working.') @@ -35,8 +35,20 @@ once(fromBackground, 'Versions', async () => { const createPanel = () => new Promise((resolve, reject) => { - chrome.devtools.panels.create('Solid', icons.normal[32], 'index.html', newPanel => { + const onCreate = (newPanel: chrome.devtools.panels.ExtensionPanel) => { if (chrome.runtime.lastError) reject(chrome.runtime.lastError) else resolve(newPanel) - }) + } + + if (import.meta.env.BROWSER === 'firefox') { + chrome.devtools.panels.create( + 'Solid', + /* firefox requires absolute paths */ + '/' + icons.disabled[32], + '/index.html', + onCreate, + ) + } else { + chrome.devtools.panels.create('Solid', icons.disabled[32], 'index.html', onCreate) + } }) diff --git a/packages/extension/env.d.ts b/packages/extension/env.d.ts index 278a1497..5a8b3704 100644 --- a/packages/extension/env.d.ts +++ b/packages/extension/env.d.ts @@ -7,6 +7,7 @@ declare global { // import.meta.env.EXPECTED_CLIENT interface ImportMetaEnv { EXPECTED_CLIENT: string + BROWSER: 'chrome' | 'firefox' } } diff --git a/packages/extension/index.html b/packages/extension/index.html index bf92e56a..8eaf6712 100644 --- a/packages/extension/index.html +++ b/packages/extension/index.html @@ -1,18 +1,16 @@ - + + + + + + Solid Devtools + - - - - - Solid Devtools - - - - -
- - - + + +
+ + diff --git a/packages/extension/manifest.ts b/packages/extension/manifest.ts deleted file mode 100644 index 522fbce4..00000000 --- a/packages/extension/manifest.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { defineManifest } from '@crxjs/vite-plugin' -import { version } from './package.json' -import icons from './src/icons' - -// Convert from Semver (example: 0.1.0-beta6) -const [major, minor, patch, label = '0'] = version - // can only contain digits, dots, or dash - .replace(/[^\d.-]+/g, '') - // split into version parts - .split(/[.-]/) - -export default defineManifest(env => ({ - manifest_version: 3, - name: `${env.mode === 'production' ? '' : '[DEV] '}Solid Devtools`, - description: 'Chrome Developer Tools extension for debugging SolidJS applications.', - homepage_url: 'https://github.com/thetarnav/solid-devtools', - // up to four numbers separated by dots - version: `${major}.${minor}.${patch}.${label}`, - // semver is OK in "version_name" - version_name: version, - author: 'Damian Tarnawski', - minimum_chrome_version: '94', - devtools_page: 'devtools/devtools.html', - content_scripts: [ - { - matches: ['*://*/*'], - js: ['content/content.ts'], - run_at: 'document_start', - }, - ], - background: { - service_worker: 'background/background.ts', - type: 'module', - }, - permissions: [], - action: { - default_icon: icons.disabled, - default_title: 'Solid Devtools', - default_popup: 'popup/popup.html', - }, - icons: icons.normal, -})) diff --git a/packages/extension/package.json b/packages/extension/package.json index e8f196df..66da4071 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -4,20 +4,18 @@ "private": true, "license": "MIT", "author": "Damian Tarnawski ", - "contributors": [], "type": "module", "scripts": { - "dev": "vite --port 3333", - "build": "npm run clean && vite build && node scripts/zip.cjs", - "clean": "rimraf dist dist.zip", - "test:unit": "vitest", + "dev": "vite", + "build:chrome": "tsx build.ts --browser=chrome", + "build:firefox": "tsx build.ts --browser=firefox", + "build": "tsx build.ts --browser=firefox --browser=chrome", + "test:unit": "echo \"No unit tests\"", "test:types": "tsc --noEmit --paths null" }, "devDependencies": { - "@crxjs/vite-plugin": "2.0.0-beta.17", - "@types/chrome": "^0.0.248", - "rimraf": "^5.0.5", - "zip-a-folder": "^3.1.3" + "@crxjs/vite-plugin": "2.0.0-beta.21", + "@types/chrome": "^0.0.248" }, "dependencies": { "@solid-devtools/debugger": "workspace:^", @@ -32,5 +30,5 @@ "vite": "^4" } }, - "packageManager": "pnpm@8.6.0" + "packageManager": "pnpm@8.9.0" } diff --git a/packages/extension/src/index.tsx b/packages/extension/panel/panel.tsx similarity index 95% rename from packages/extension/src/index.tsx rename to packages/extension/panel/panel.tsx index 139ae7af..807d426d 100644 --- a/packages/extension/src/index.tsx +++ b/packages/extension/panel/panel.tsx @@ -1,8 +1,12 @@ +/* + Devtools panel entry point +*/ + import { Debugger } from '@solid-devtools/debugger/types' import { createDevtools, MountIcons } from '@solid-devtools/frontend' import { createSignal } from 'solid-js' import { render } from 'solid-js/web' -import { ConnectionName, createPortMessanger, once, Versions } from './bridge' +import { ConnectionName, createPortMessanger, once, Versions } from '../shared/bridge' import '@solid-devtools/frontend/dist/styles.css' @@ -12,9 +16,6 @@ const { postPortMessage: toBackground, onPortMessage: fromBackground } = createP Debugger.InputChannels >(port) -// Force debugger to send state on connect -toBackground('ResetState') - function App() { const [versions, setVersions] = createSignal({ solid: '', diff --git a/packages/extension/popup/popup.html b/packages/extension/popup/popup.html index 7176d8dc..863cfc37 100644 --- a/packages/extension/popup/popup.html +++ b/packages/extension/popup/popup.html @@ -1,16 +1,14 @@ - + + + + + + Solid Devtools + - - - - - Solid Devtools - - - -
- - - + +
+ + diff --git a/packages/extension/popup/index.tsx b/packages/extension/popup/popup.tsx similarity index 96% rename from packages/extension/popup/index.tsx rename to packages/extension/popup/popup.tsx index 6aca8ba7..13dc7b02 100644 --- a/packages/extension/popup/index.tsx +++ b/packages/extension/popup/popup.tsx @@ -2,7 +2,13 @@ import { Accessor, Component, JSX, Show, createSignal } from 'solid-js' import { render } from 'solid-js/web' -import { ConnectionName, DetectionState, Versions, createPortMessanger, once } from '../src/bridge' +import { + ConnectionName, + DetectionState, + Versions, + createPortMessanger, + once, +} from '../shared/bridge' import './popup.css' diff --git a/packages/extension/scripts/zip.cjs b/packages/extension/scripts/zip.cjs deleted file mode 100644 index 179b4b4c..00000000 --- a/packages/extension/scripts/zip.cjs +++ /dev/null @@ -1 +0,0 @@ -require('zip-a-folder').zip('dist', 'dist.zip') diff --git a/packages/extension/src/bridge.ts b/packages/extension/shared/bridge.ts similarity index 95% rename from packages/extension/src/bridge.ts rename to packages/extension/shared/bridge.ts index a2b8481e..95d5b0a3 100644 --- a/packages/extension/src/bridge.ts +++ b/packages/extension/shared/bridge.ts @@ -26,6 +26,9 @@ export type DetectEvent = { state: DetectionState } +const LOG_MESSAGES = import.meta.env.DEV +// const LOG_MESSAGES: boolean = true + export function createPortMessanger< IM extends { [K in string]: any } = {}, OM extends { [K in string]: any } = {}, @@ -42,10 +45,9 @@ export function createPortMessanger< } = {} let connected = true - import.meta.env.DEV && log(`${port.name.replace(DEVTOOLS_ID_PREFIX, '')} port connected.`) + LOG_MESSAGES && log(`${port.name.replace(DEVTOOLS_ID_PREFIX, '')} port connected.`) port.onDisconnect.addListener(() => { - import.meta.env.DEV && - log(`${port.name.replace(DEVTOOLS_ID_PREFIX, '')} port disconnected.`) + LOG_MESSAGES && log(`${port.name.replace(DEVTOOLS_ID_PREFIX, '')} port disconnected.`) connected = false listeners = {} port.onMessage.removeListener(onMessage) diff --git a/packages/extension/src/icons.ts b/packages/extension/shared/icons.ts similarity index 95% rename from packages/extension/src/icons.ts rename to packages/extension/shared/icons.ts index 3a2f058c..da037ad9 100644 --- a/packages/extension/src/icons.ts +++ b/packages/extension/shared/icons.ts @@ -12,5 +12,3 @@ export const icons = { 128: 'assets/icons/solid-gray-128.png', }, } as const - -export default icons diff --git a/packages/extension/src/event-bus.ts b/packages/extension/src/event-bus.ts deleted file mode 100644 index 2c560483..00000000 --- a/packages/extension/src/event-bus.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type Listener = (payload: T) => void - -export type Listen = (listener: Listener) => VoidFunction - -export type Emit = (..._: void extends T ? [payload?: T] : [payload: T]) => void - -export class EventBus extends Set> { - emit(..._: void extends T ? [payload?: T] : [payload: T]): void - emit(payload?: any) { - for (const cb of this) cb(payload) - } -} diff --git a/packages/extension/tsconfig.json b/packages/extension/tsconfig.json index b9424c4d..32779884 100644 --- a/packages/extension/tsconfig.json +++ b/packages/extension/tsconfig.json @@ -7,7 +7,7 @@ } }, "include": [ - "src/**/*", + "panel/**/*", "background/**/*", "content/**/*", "devtools/**/*", diff --git a/packages/extension/vite.config.ts b/packages/extension/vite.config.ts index 77fa7268..bb0ac93b 100644 --- a/packages/extension/vite.config.ts +++ b/packages/extension/vite.config.ts @@ -1,25 +1,89 @@ -import { crx } from '@crxjs/vite-plugin' -import fs from 'fs' -import { createRequire } from 'node:module' -import path from 'path' +import * as crx from '@crxjs/vite-plugin' +import fs from 'node:fs' +import module from 'node:module' +import path from 'node:path' +import * as vite from 'vite' import solidPlugin from 'vite-plugin-solid' -import { defineConfig, UserConfig } from 'vitest/config' -import { testConfig } from '../../configs/vitest.config' -import manifest from './manifest' +import ext_pkg from './package.json' +import { icons } from './shared/icons.js' -const require = createRequire(import.meta.url) +const require = module.createRequire(import.meta.url) const cwd = process.cwd() -const solidDevtoolsPkg = JSON.parse( - fs.readFileSync(require.resolve('solid-devtools/package.json'), 'utf-8'), -) as { version: string } +const browser = process.env['BROWSER'] ?? 'chrome' +if (browser !== 'chrome' && browser !== 'firefox') { + throw new Error('browser arg must be "chrome" or "firefox", was ' + browser) +} +const is_chrome = browser === 'chrome' -const solidDevtoolsVersion = JSON.stringify(solidDevtoolsPkg.version.match(/\d+.\d+.\d+/)![0]) +const manifest_version = (() => { + // Convert from Semver (example: 0.1.0-beta6) + const [major, minor, patch, label = '0'] = ext_pkg.version + // can only contain digits, dots, or dash + .replace(/[^\d.-]+/g, '') + // split into version parts + .split(/[.-]/) -export default defineConfig(config => { - const isDev = config.mode === 'development' + return `${major}.${minor}.${patch}.${label}` +})() + +const manifest = crx.defineManifest(env => { + type Manifest = Exclude | ((...a: any[]) => any)> & { + browser_specific_settings?: Record> + } + + const manifest: Manifest = { + manifest_version: 3, + name: `${env.mode === 'development' ? '[DEV] ' : ''}Solid Devtools`, + description: 'Chrome Developer Tools extension for debugging SolidJS applications.', + homepage_url: 'https://github.com/thetarnav/solid-devtools', + version: manifest_version, + version_name: is_chrome ? ext_pkg.version : undefined, + browser_specific_settings: is_chrome + ? undefined + : { gecko: { id: '{abfd162e-9948-403a-a75c-6e61184e1d47}' } }, + author: 'Damian Tarnawski', + minimum_chrome_version: '94', + devtools_page: 'devtools/devtools.html', + content_scripts: [ + { + matches: ['*://*/*'], + js: ['content/content.ts'], + run_at: 'document_start', + }, + ], + background: is_chrome + ? { + service_worker: 'background/background.ts', + type: 'module', + } + : { + scripts: ['background/background.ts'], + type: 'module', + }, + permissions: [], + action: { + default_icon: icons.disabled, + default_title: 'Solid Devtools', + default_popup: 'popup/popup.html', + }, + icons: icons.normal, + } + + return manifest as any +}) + +export default vite.defineConfig(config => { + const is_dev = config.mode === 'development' + + const sdt_pkg = JSON.parse( + fs.readFileSync(require.resolve('solid-devtools/package.json'), 'utf-8'), + ) as { version: string } + + const sdt_version = JSON.stringify(sdt_pkg.version.match(/\d+.\d+.\d+/)![0]) return { + server: { port: 3333 }, resolve: { alias: { 'solid-js/web': path.resolve(cwd, 'node_modules/solid-js/web/dist/web.js'), @@ -28,29 +92,36 @@ export default defineConfig(config => { }, }, plugins: [ - solidPlugin({ dev: false, hot: false }) as any, - crx({ manifest }), + solidPlugin({ dev: false, hot: false }), + crx.crx({ + manifest: manifest, + browser: browser, + }), { name: 'replace-version', enforce: 'pre', transform(code, id) { if (id.includes('solid-devtools')) { - return code.replace( - /import\.meta\.env\.EXPECTED_CLIENT/g, - solidDevtoolsVersion, - ) + code = code.replace(/import\.meta\.env\.EXPECTED_CLIENT/g, sdt_version) } return code }, }, ], + define: { + 'import.meta.env.BROWSER': JSON.stringify(browser), + }, build: { - emptyOutDir: !isDev, + minify: false, + emptyOutDir: !is_dev, + outDir: 'dist/' + browser, rollupOptions: { input: { panel: 'index.html' }, }, target: 'esnext', }, - test: testConfig as any, - } satisfies UserConfig + optimizeDeps: { + exclude: ['@solid-devtools/debugger'], + }, + } }) diff --git a/packages/extension/web_ext_run b/packages/extension/web_ext_run new file mode 100755 index 00000000..ed0eec9c --- /dev/null +++ b/packages/extension/web_ext_run @@ -0,0 +1,3 @@ +#!/bin/bash + +web-ext run -s dist/firefox -p dev "$@" diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 42b3771c..7df86a0a 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -37,6 +37,8 @@ "test:types": "tsc --noEmit" }, "dependencies": { + "@nothing-but/utils": "~0.11.1", + "@solid-devtools/theme": "workspace:^", "@solid-devtools/debugger": "workspace:^", "@solid-devtools/shared": "workspace:^", "@solid-primitives/context": "^0.2.1", @@ -67,5 +69,5 @@ "peerDependencies": { "solid-js": "^1.8.0" }, - "packageManager": "pnpm@8.6.0" + "packageManager": "pnpm@8.9.0" } diff --git a/packages/frontend/src/modules/structure/structure-tree.tsx b/packages/frontend/src/modules/structure/structure-tree.tsx index 5e5af36f..767696db 100644 --- a/packages/frontend/src/modules/structure/structure-tree.tsx +++ b/packages/frontend/src/modules/structure/structure-tree.tsx @@ -253,20 +253,26 @@ const DisplayStructureTree: Component = () => { lastInspectedIndex, ) - const nodeList = structure.state().nodeList - const collapsedList: Structure.Node[] = [] + const all_nodes_list = structure.state().nodeList + const collapsed_list: Structure.Node[] = [] const set = collapsed() - let skip = 0 - for (const node of nodeList) { - const skipped = skip > 0 - if (skipped) skip-- - else collapsedList.push(node) + /* + Go over the list of all nodes + skip the ones that are in collapsed set + by increasing the skip counter by the number of children + */ + let skip_n = 0 + for (const node of all_nodes_list) { + const is_skipped = skip_n > 0 - if (skipped || set.has(node)) skip += node.children.length + if (is_skipped) skip_n-- + else collapsed_list.push(node) + + if (is_skipped || set.has(node)) skip_n += node.children.length } - return collapsedList + return collapsed_list }) const virtual = createMemo<{ diff --git a/packages/frontend/src/ui/index.ts b/packages/frontend/src/ui/index.ts index d4bb5d9e..e38dc83a 100644 --- a/packages/frontend/src/ui/index.ts +++ b/packages/frontend/src/ui/index.ts @@ -1,4 +1,4 @@ -export * as theme from '../../../../configs/theme' +export * as theme from '@solid-devtools/theme' export * as styles from './styles' export * from './components/badge' diff --git a/packages/frontend/src/ui/styles.tsx b/packages/frontend/src/ui/styles.tsx index 494dc71d..72d5b8fc 100644 --- a/packages/frontend/src/ui/styles.tsx +++ b/packages/frontend/src/ui/styles.tsx @@ -1,7 +1,7 @@ import { value_node_styles } from '@/modules/inspector/value-node' import { owner_path_styles } from '@/modules/structure' import { theme } from '@/ui' -import { make_var_styles } from '../../../../configs/theme' +import { make_var_styles } from '@solid-devtools/theme' import { highlight_styles } from './components/highlight' import { custom_scrollbar_styles } from './components/scrollable' import { toggle_button_styles } from './components/toggle-button' diff --git a/packages/overlay/package.json b/packages/overlay/package.json index 47d488ed..d0df366a 100644 --- a/packages/overlay/package.json +++ b/packages/overlay/package.json @@ -47,6 +47,7 @@ "test:types": "tsc --noEmit --paths null" }, "dependencies": { + "@nothing-but/utils": "~0.11.1", "@solid-devtools/debugger": "workspace:^", "@solid-devtools/frontend": "workspace:^", "@solid-devtools/shared": "workspace:^", diff --git a/packages/theme/package.json b/packages/theme/package.json new file mode 100644 index 00000000..818f5b0d --- /dev/null +++ b/packages/theme/package.json @@ -0,0 +1,46 @@ +{ + "name": "@solid-devtools/theme", + "version": "0.0.0", + "license": "MIT", + "author": "Damian Tarnawski ", + "contributors": [], + "homepage": "https://github.com/thetarnav/solid-devtools/tree/main/packages/theme#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/thetarnav/solid-devtools.git" + }, + "bugs": { + "url": "https://github.com/thetarnav/solid-devtools/issues" + }, + "private": false, + "publishConfig": { + "access": "public" + }, + "sideEffects": false, + "files": [ + "./theme.ts" + ], + "type": "module", + "module": "./theme.ts", + "types": "./theme.ts", + "exports": { + "types": "./theme.ts", + "import": "./theme.ts" + }, + "scripts": { + "dev": "echo \"No dev script\"", + "build": "echo \"No build script\"", + "test:unit": "echo \"No test:unit script\"", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@nothing-but/utils": "~0.11.1" + }, + "packageManager": "pnpm@8.9.0", + "keywords": [ + "solid", + "devtools", + "css", + "unocss" + ] +} diff --git a/configs/theme.ts b/packages/theme/theme.ts similarity index 100% rename from configs/theme.ts rename to packages/theme/theme.ts diff --git a/packages/theme/tsconfig.json b/packages/theme/tsconfig.json new file mode 100644 index 00000000..1cf62f72 --- /dev/null +++ b/packages/theme/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./theme.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d54ac7c0..61a2c023 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,12 +11,12 @@ importers: '@changesets/cli': specifier: ^2.26.2 version: 2.26.2 - '@nothing-but/utils': - specifier: ~0.11.1 - version: 0.11.1 '@playwright/test': specifier: ^1.39.0 version: 1.39.0 + '@solid-devtools/theme': + specifier: file:packages/theme + version: file:packages/theme '@total-typescript/ts-reset': specifier: ^0.5.1 version: 0.5.1 @@ -68,6 +68,9 @@ importers: tsup-preset-solid: specifier: ^2.1.0 version: 2.1.0(esbuild@0.19.5)(solid-js@1.8.5)(tsup@7.2.0) + tsx: + specifier: ^3.14.0 + version: 3.14.0 turbo: specifier: ^1.10.16 version: 1.10.16 @@ -221,8 +224,8 @@ importers: version: 1.8.5 devDependencies: '@crxjs/vite-plugin': - specifier: 2.0.0-beta.17 - version: 2.0.0-beta.17 + specifier: 2.0.0-beta.21 + version: 2.0.0-beta.21 '@types/chrome': specifier: ^0.0.248 version: 0.0.248 @@ -235,12 +238,18 @@ importers: packages/frontend: dependencies: + '@nothing-but/utils': + specifier: ~0.11.1 + version: 0.11.1 '@solid-devtools/debugger': specifier: workspace:^ version: link:../debugger '@solid-devtools/shared': specifier: workspace:^ version: link:../shared + '@solid-devtools/theme': + specifier: workspace:^ + version: link:../theme '@solid-primitives/context': specifier: ^0.2.1 version: 0.2.1(solid-js@1.8.5) @@ -382,6 +391,9 @@ importers: packages/overlay: dependencies: + '@nothing-but/utils': + specifier: ~0.11.1 + version: 0.11.1 '@solid-devtools/debugger': specifier: workspace:^ version: link:../debugger @@ -454,6 +466,12 @@ importers: specifier: ^1.8.5 version: 1.8.5 + packages/theme: + dependencies: + '@nothing-but/utils': + specifier: ~0.11.1 + version: 0.11.1 + packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -1781,8 +1799,8 @@ packages: prettier: 2.8.8 dev: true - /@crxjs/vite-plugin@2.0.0-beta.17: - resolution: {integrity: sha512-44CavnsY01jvF2uHfRdqA3j6bg6IcVc+ZrSayYS+H7eqb6BHWh619A2jJ3Y0eHVh9H65041ob4g6I1VMxWx4qw==} + /@crxjs/vite-plugin@2.0.0-beta.21: + resolution: {integrity: sha512-kSXgHHqCXASqJ8NmY94+KLGVwdtkJ0E7KsRQ+vbMpRliJ5ze0xnSk0l41p4txlUysmEoqaeo4Xb7rEFdcU2zjQ==} dependencies: '@rollup/pluginutils': 4.2.1 '@webcomponents/custom-elements': 1.6.0 @@ -7991,3 +8009,10 @@ packages: compress-commons: 5.0.1 readable-stream: 3.6.2 dev: true + + file:packages/theme: + resolution: {directory: packages/theme, type: directory} + name: '@solid-devtools/theme' + dependencies: + '@nothing-but/utils': 0.11.1 + dev: true diff --git a/tsconfig.json b/tsconfig.json index 3ae37e5d..77497a8e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,12 @@ { - "extends": "./configs/tsconfig.base.json", - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "jsx": "preserve", - "jsxImportSource": "solid-js", - "declarationMap": true, - "declaration": true - }, - "exclude": ["node_modules", "**/dist/**/*"] + "extends": "./configs/tsconfig.base.json", + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "jsx": "preserve", + "jsxImportSource": "solid-js", + "declarationMap": true, + "declaration": true, + "rootDir": "." + }, + "exclude": ["node_modules", "**/dist/**/*"] } diff --git a/uno.config.ts b/uno.config.ts index 51d11ec8..0e93e730 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -1,8 +1,8 @@ -import { defineConfig, presetUno } from 'unocss' -import * as theme from './configs/theme' +import * as theme from '@solid-devtools/theme' +import * as uno from 'unocss' -export default defineConfig({ - presets: [presetUno({ dark: 'media' })], +export default uno.defineConfig({ + presets: [uno.presetUno({ dark: 'media' })], theme: { colors: theme.colors,