diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 258dbb2..1470f56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,14 +49,27 @@ jobs: - name: Test run: pnpm test - - name: Build - run: pnpm build + - name: Build Firefox + run: pnpm build:firefox + + # AMO validates the bundle on submission, so catching a manifest it rejects here beats + # finding out during a release. Warnings are informational; only errors fail the step. + - name: Validate Firefox bundle + run: pnpm dlx web-ext@10 lint --source-dir dist-firefox --self-hosted e2e: - name: E2E + name: E2E (${{ matrix.project }}) + # A same-repo PR is already covered by the push event on its branch, so only pushes and fork PRs + # need a run. if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository timeout-minutes: 20 runs-on: ubuntu-24.04 + needs: build + + strategy: + fail-fast: false + matrix: + project: [chrome, firefox] steps: - name: Checkout @@ -68,8 +81,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: '8.5' - tools: composer - extensions: mbstring, dom, xml, curl, fileinfo, tokenizer, ctype, filter, session, openssl, intl, bcmath, sqlite3, pdo_sqlite + extensions: pdo_sqlite - name: Install pnpm uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 @@ -81,31 +93,41 @@ jobs: with: node-version: 24 cache: pnpm + # The e2e app is a second pnpm project with its own lockfile, so both belong in the key or + # its install never comes from the store. + cache-dependency-path: | + pnpm-lock.yaml + tests/e2e/app/pnpm-lock.yaml - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Install Playwright Chromium - run: pnpm exec playwright install --with-deps chromium - - # Bootstrap the app as explicit steps (not only inside the Playwright webServer - # command) so composer/boot failures surface here with full, untruncated output. - # The app runs through the vite dev server (started by Playwright), so no build here. - - name: Bootstrap e2e app - run: bash tests/e2e/app/setup.sh + # No browser or driver install step: Selenium Manager, bundled with selenium-webdriver, fetches + # both and pairs them in the browser launchers. This only keeps them between runs. + - name: Cache Selenium browsers and drivers + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/selenium + key: selenium-${{ runner.os }}-${{ matrix.project }}-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + selenium-${{ runner.os }}-${{ matrix.project }}- - name: Install e2e app deps - run: pnpm --dir tests/e2e/app install + run: pnpm --dir tests/e2e/app install --frozen-lockfile - name: Run e2e tests - run: pnpm exec playwright test -c tests/e2e/playwright.config.ts + run: pnpm test:e2e:${{ matrix.project }} - # Screenshots and the first-retry trace are the only way to see what a failing run - # actually did: the log names these files but the runner throws them away. - - name: Upload Playwright artifacts + # The fixture captures the active Selenium URL, title and screenshot plus window handles and + # browser warnings before teardown. These attachments are the CI failure evidence because + # Playwright does not launch or trace the browser. + - name: Upload failure artifacts if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: playwright-results - path: test-results/ + name: e2e-failures-${{ matrix.project }} + path: tests/e2e/test-results + # Discovery/setup failures can happen before Playwright creates its results directory. + # The test step has already failed, so do not replace its useful error with an upload error. + if-no-files-found: warn retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06f4127..2b8f4bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,8 +37,11 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Build - run: pnpm build + - name: Build Chrome + run: pnpm build:chrome + + - name: Build Firefox + run: pnpm build:firefox - name: Package and publish id: publish @@ -46,9 +49,11 @@ jobs: GH_TOKEN: ${{ github.token }} run: | version="${GITHUB_REF_NAME#v}" - zip="inertia-devtools-extension-${version}.zip" - (cd dist && zip -qr "../${zip}" .) - gh release create "$GITHUB_REF_NAME" "$zip" --generate-notes + chrome_zip="inertia-devtools-extension-chrome-${version}.zip" + firefox_zip="inertia-devtools-extension-firefox-${version}.zip" + (cd dist-chrome && zip -qr "../${chrome_zip}" .) + (cd dist-firefox && zip -qr "../${firefox_zip}" .) + gh release create "$GITHUB_REF_NAME" "$chrome_zip" "$firefox_zip" --generate-notes notes="$(gh release view "$GITHUB_REF_NAME" --json body --jq .body)" { diff --git a/.gitignore b/.gitignore index 427062d..82890b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules -dist +dist-chrome +dist-firefox test-results *.zip inertia-devtools-extension-*/ diff --git a/BROWSERS.md b/BROWSERS.md new file mode 100644 index 0000000..24f0354 --- /dev/null +++ b/BROWSERS.md @@ -0,0 +1,169 @@ +# Browsers + +Chrome and Firefox ship from the same sources. `manifest.config.ts` generates a manifest per target +and `vite.config.ts` writes each build into its own directory. Neither browser is the default: every +build names its target, so there is no bare `pnpm build`. + +```bash +pnpm build:chrome # production build into dist-chrome/ +pnpm dev:chrome # watch build into dist-chrome/ + +pnpm build:firefox # production build into dist-firefox/ +pnpm dev:firefox # watch build into dist-firefox/ +``` + +Load the Chrome build through `chrome://extensions` → "Load unpacked" → pick `dist-chrome/`. Load +the Firefox one through `about:debugging#/runtime/this-firefox` → "Load Temporary Add-on" → pick +`dist-firefox/manifest.json`. Temporary add-ons are removed when Firefox closes. + +Validate the Firefox bundle the way [addons.mozilla.org](https://addons.mozilla.org/developers/) +(AMO, Mozilla's add-on store) will. CI runs this on every build, and it is worth running by hand +after touching the manifest: + +```bash +pnpm dlx web-ext@10 lint --source-dir dist-firefox --self-hosted +``` + +Two warnings are expected and harmless, so they do not fail the run: `UNSAFE_VAR_ASSIGNMENT` (Vue's +runtime assigns `innerHTML`) and `KEY_FIREFOX_ANDROID_UNSUPPORTED_BY_MIN_VERSION` (Firefox for +Android has no DevTools, so the panel is desktop-only regardless). Anything the validator counts as +an error does fail it. + +## Where the targets differ + +Chrome is the browser the code was written against, so every entry below is a Firefox constraint the +Chrome build inherits. + +- **No service worker.** Firefox has no MV3 `background.service_worker` ([bug 1573659](https://bugzil.la/1573659)), + so its manifest points `background.scripts` at the same `background.js`, which runs as an event + page. It is bundled dependency-free for Chrome already, which is what makes that possible. Event + pages are suspended when idle just like the worker, so nothing may assume in-memory state + survives. +- **No DNR enum objects.** Chrome exposes `declarativeNetRequest.ResourceType`, `RuleActionType` and + `HeaderOperation` as runtime enums; Firefox does not. `src/background/tabRules.ts` uses the wire + strings both accept, because reading a missing enum member would throw inside the `try` and + silently leave the tab header rule uninstalled. +- **Host permissions are revocable.** Firefox prompts for `` at install, but the user can + turn access off per site at any time, which kills content scripts, `webRequest` and the DNR rule in + one go. `src/panel/lib/useHostAccess.ts` detects that and the panel shows a banner instead of + looking like an app without a recorder. +- **The DevTools page is resolved relatively.** Firefox resolves the panel path against the + DevTools page's own URL rather than the extension root, and rejects an extension-absolute URL + outright, so `devtools.html` sits at the root of the build to keep one relative path correct in + both browsers. The panel icon stays empty: Chrome renders the title alone and Firefox falls back to + the manifest icon. +- **The panel's `system` theme means a different thing per browser.** It resolves + `prefers-color-scheme`, and each browser answers that from somewhere else inside a toolbox. Firefox + answers with the DevTools theme, so a light toolbox on a dark desktop renders a light panel, and the + desktop never enters into it. Chrome answers with the OS, so its panel follows the desktop live. Two + settings that look like they should reach the panel do not: Firefox's "Website appearance", which + overrides content documents while a toolbox panel is not one, and the DevTools theme in Chrome. Reading + `devtools.panels.themeName` instead was tried and reverted, since in Firefox the media query already + reports exactly that, and Chrome offers no `onThemeChanged` to keep a reading fresh. The theme toggle + in the panel header overrides all of it. +- **No split incognito.** Chrome gets `"incognito": "split"`, so a private window runs its own worker + and mints its own tab identities and no recording crosses that boundary. Firefox reads `split` as + `not_allowed` ([bug 1380812](https://bugzil.la/1380812)), which would disable the panel in a private + window outright, so its manifest omits the key and keeps the spanning default. +- **`world: "MAIN"` sets the version floor.** Firefox honours it from 128. Its manifest asks for 140 + because that is where `data_collection_permissions` is understood. Chrome's floor is 116, where MV3 + service workers and DNR session rules landed. + +## Automated coverage + +One suite covers both browsers. The specs in `tests/e2e/shared` never name a browser: the Playwright +project name picks a driver, so the same test runs twice. Specs that need a browser-only capability +live under `tests/e2e/firefox`, and the project config includes those only in Firefox, so `shared/` +stays honest about running in both browsers. + +```bash +pnpm test:e2e # both browser projects +pnpm test:e2e:chrome # Chrome only +pnpm test:e2e:firefox # Firefox only +``` + +Each test launches a fresh browser session and profile, and its fixture closes that exact runtime in +teardown. Both projects may run in one Playwright process because Selenium and the operating system +own the transport endpoints; the harness has no endpoint allocator. + +Playwright is the test runner here, not the browser: it loads extensions into Chromium alone, so both +browsers are driven through `selenium-webdriver` (`tests/e2e/drivers/`). Selenium Manager, which ships +inside that package, downloads and caches both browsers and both drivers as matched pairs, so there is +nothing to install and no version to keep in step. Each browser launcher sets +`SE_FORCE_BROWSER_DOWNLOAD` so Selenium Manager downloads rather than picking up a local install, +and that matters: + +- Stable Chrome refuses `--load-extension`. A local install starts fine and silently carries no + extension, so the tests need Chrome for Testing. +- Playwright's bundled Firefox does not inject extension content scripts at all. Entries still arrive, + because those come from `webRequest` in the background, while page state stays empty and every + `visitId` and `batchId` is null. `shared/instrumentation.spec.ts` asserts a `visitId` precisely so + that a browser which drops content scripts fails instead of reporting green. + +Background state is read through the messages the panel itself uses (`panel:hydrate` and +`panel:hydrate-page-state`), sent from an extension page. A Chrome service worker and a Firefox event +page answer those identically, so no browser debugging protocol is involved. Both are started lazily, +which is why a session waits for the background to answer before the first navigation: +`webRequest.onHeadersReceived` is what records an entry, and a navigation that beats it awake is +never seen. + +An unexpected test result triggers proportional failure capture before teardown. The fixture records +the active URL and title, all window handles, browser warnings, and one active-window screenshot. +Each read is best-effort, so diagnostic failure does not replace the original test failure. + +Firefox retains two privileged WebDriver seams. Geckodriver's `--allow-system-access` service switch +allows a temporary extension page to be opened from Firefox's browser context and allows the +Firefox-only `devtools-panel.spec.ts` to open the real toolbox, find and select the registered Inertia +tool, and wait for its panel to render. Page warnings come from WebDriver BiDi. + +The rest of the harness is functional: `app.ts`, `extension.ts`, and `panel.ts` expose the small +cross-browser operations used by scenarios, `waits.ts` owns observation helpers, and `fixtures.ts` +creates and tears down one runtime per test. + +Chrome still has one explicit manual boundary. Its headless DevTools frontend runs the extension's +DevTools entry page but does not expose custom panels to WebDriver. A unit test therefore locks the +`panels.create` call and tab-specific URL, while the shared suite exercises the built panel directly. +The first manual smoke step remains the proof that Chrome registers the panel in the real toolbox. + +## Manual smoke checklist + +Run `pnpm build:firefox`, load the add-on, start the e2e app +(`tests/e2e/app`: `php artisan serve --port=13337` plus its own `pnpm dev` on `:4242`) or any Inertia +app in dev mode, then walk through: + +1. **Recording.** Open DevTools → Inertia tab, navigate the app. Entries appear with status, method, + component and duration. This alone proves `webRequest.onHeadersReceived` sees the + `x-inertia-devtools-id` header and that the background fetch to + `{origin}/_inertia/devtools/entries/{id}` returns with credentials over plain HTTP. +2. **Tab header.** Reload once (the first response of a newly proven host is unstamped), then check + the app receives `x-inertia-devtools-tab` on later requests. This is the DNR session rule; if the + header never arrives the rule was rejected. +3. **Props and page state.** Props tab shows values with prop-type metadata, Page tab shows the + client page object. Proves the MAIN-world script, the postMessage bridge and pairing. +4. **Lineage and batching.** Trigger a partial reload, a deferred prop and a prefetch-then-visit. + Rows group into batches and the prefetch is marked consumed. Proves the interceptor registry is + reachable from the MAIN world. +5. **Dev-mode banner.** Point at a production build of an app and confirm the "not running in dev + mode" banner appears rather than an empty panel. +6. **Host access banner.** Extensions button → turn off access for the site → the panel shows the + "no access to this site" banner. Turn it back on and it disappears without reopening DevTools. +7. **Suspension.** Leave DevTools open and idle for a few minutes, then navigate again: new entries + still arrive after the event page has been suspended and restarted. + +## Store submission + +The Release workflow attaches both zips to the GitHub release: +`inertia-devtools-extension-chrome-.zip` goes to the +[Chrome Web Store](https://chrome.google.com/webstore/devconsole) and +`inertia-devtools-extension-firefox-.zip` to AMO. Both are uploaded by hand. + +AMO needs two things Chrome does not: + +- The bundle is minified, so a source-code archive plus build instructions go with the submission: + Node 24, pnpm 11, `pnpm install --frozen-lockfile && pnpm build:firefox`, output in + `dist-firefox/`. +- `browser_specific_settings.gecko.data_collection_permissions` declares `none`. Keep it accurate: + everything the panel shows is fetched from the inspected app and stays on the machine. + +The Gecko extension id and the version floor live in `manifest.config.ts` and must not change once +listed. diff --git a/README.md b/README.md index 2c4e7b9..79b8821 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,19 @@ -# Inertia.js DevTools for Chrome +# Inertia.js DevTools -Chrome DevTools panel for inspecting Inertia.js requests, page props, route metadata, response bodies, and client-side page state. +Browser DevTools panel for inspecting Inertia.js requests, page props, route metadata, response bodies, and client-side page state. Runs in Chrome and Firefox. Full documentation, including installation and enabling the server-side recorder, lives at [inertiajs.com/docs/devtools](https://inertiajs.com/docs/devtools). +Both browsers build from the same sources, each into its own directory: + +```bash +pnpm build:chrome # dist-chrome/ +pnpm build:firefox # dist-firefox/ +``` + +Loading a build, where the two targets differ, and how each one is submitted to its store are +covered in [BROWSERS.md](BROWSERS.md). + ## License Inertia.js DevTools is open-sourced software licensed under the MIT license. diff --git a/manifest.config.ts b/manifest.config.ts new file mode 100644 index 0000000..0f3f12f --- /dev/null +++ b/manifest.config.ts @@ -0,0 +1,128 @@ +import { readFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +export type ExtensionTarget = 'chrome' | 'firefox' + +export type ExtensionManifest = { + manifest_version: 3 + name: string + version: string + description: string + devtools_page: string + icons: Record + action: { + default_popup: string + default_title: string + default_icon: Record + } + permissions: string[] + host_permissions: string[] + content_scripts: Array<{ + matches: string[] + js: string[] + run_at: string + world?: 'MAIN' + }> + background: { service_worker: string } | { scripts: string[] } + content_security_policy?: { extension_pages: string } + incognito?: 'split' + minimum_chrome_version?: string + browser_specific_settings?: { + gecko: { + id: string + strict_min_version: string + data_collection_permissions: { required: string[] } + } + } +} + +/** Keep the Gecko ID stable because it identifies extension storage and the AMO listing. */ +const FIREFOX_EXTENSION_ID = 'devtools@inertiajs.com' + +/** Firefox 140 is the first ESR supporting AMO's data-collection declaration. */ +const FIREFOX_MIN_VERSION = '140.0' + +/** Chrome 116 supports the required MV3 worker and DNR session rules. */ +const CHROME_MIN_VERSION = '116' + +function packageVersion(): string { + const manifestPackage = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf8')) as { version: string } + + return manifestPackage.version +} + +export function buildManifest(target: ExtensionTarget): ExtensionManifest { + const base = { + manifest_version: 3 as const, + name: 'Inertia.js DevTools', + version: packageVersion(), + description: '', + devtools_page: 'devtools.html', + icons: { + '16': 'icons/icon-16.png', + '32': 'icons/icon-32.png', + '48': 'icons/icon-48.png', + '128': 'icons/icon-128.png', + }, + action: { + default_popup: 'popup/popup.html', + default_title: 'Inertia.js DevTools', + default_icon: { + '16': 'icons/icon-16.png', + '32': 'icons/icon-32.png', + '48': 'icons/icon-48.png', + '128': 'icons/icon-128.png', + }, + }, + permissions: ['storage', 'declarativeNetRequest', 'webRequest', 'tabs'], + host_permissions: ['http://*/*', 'https://*/*'], + content_scripts: [ + { + matches: ['http://*/*', 'https://*/*'], + js: ['content-script.js'], + run_at: 'document_start', + }, + { + matches: ['http://*/*', 'https://*/*'], + js: ['page-world.js'], + run_at: 'document_start', + world: 'MAIN' as const, + }, + ], + } + + if (target === 'firefox') { + return { + ...base, + description: 'Inspect every Inertia.js visit in Firefox DevTools: props, requests, routes, and page state.', + background: { scripts: ['background.js'] }, + // Firefox's default MV3 policy for extension pages carries `upgrade-insecure-requests`, which + // rewrites the entry fetch to an app on plain http (the normal local setup) to https and fails + // it. Restating the policy without that directive is the only way to opt out. + content_security_policy: { extension_pages: "script-src 'self'; object-src 'self'" }, + browser_specific_settings: { + gecko: { + id: FIREFOX_EXTENSION_ID, + strict_min_version: FIREFOX_MIN_VERSION, + // AMO requires an explicit disclosure. Everything recorded stays in the panel and the + // recorder entries are fetched from the inspected app itself, so nothing is collected. + data_collection_permissions: { required: ['none'] }, + }, + }, + } + } + + return { + ...base, + description: 'Inspect every Inertia.js visit in Chrome DevTools: props, requests, routes, and page state.', + minimum_chrome_version: CHROME_MIN_VERSION, + background: { service_worker: 'background.js' }, + // Chrome-only: an incognito window gets its own worker and its own tab identities, so recordings + // never cross that boundary. Firefox reads `split` as `not_allowed` (https://bugzil.la/1380812), + // which would disable the panel in a private window altogether, so it keeps the spanning default. + incognito: 'split', + } +} diff --git a/manifest.json b/manifest.json deleted file mode 100644 index 78fb11e..0000000 --- a/manifest.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "manifest_version": 3, - "name": "Inertia.js DevTools", - "version": "0.7.0", - "description": "Inspect every Inertia.js visit in Chrome DevTools: props, requests, routes, and page state.", - "devtools_page": "devtools/devtools.html", - "icons": { - "16": "icons/icon-16.png", - "32": "icons/icon-32.png", - "48": "icons/icon-48.png", - "128": "icons/icon-128.png" - }, - "action": { - "default_popup": "popup/popup.html", - "default_title": "Inertia.js DevTools", - "default_icon": { - "16": "icons/icon-16.png", - "32": "icons/icon-32.png", - "48": "icons/icon-48.png", - "128": "icons/icon-128.png" - } - }, - "minimum_chrome_version": "116", - "incognito": "split", - "permissions": ["storage", "declarativeNetRequest", "webRequest", "tabs"], - "host_permissions": ["http://*/*", "https://*/*"], - "background": { - "service_worker": "background.js" - }, - "content_scripts": [ - { - "matches": ["http://*/*", "https://*/*"], - "js": ["content-script.js"], - "run_at": "document_start" - }, - { - "matches": ["http://*/*", "https://*/*"], - "js": ["page-world.js"], - "run_at": "document_start", - "world": "MAIN" - } - ] -} diff --git a/package.json b/package.json index 0bb309c..b2cb768 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "@inertiajs/devtools-extension", - "version": "0.7.0", + "version": "0.8.0", "license": "MIT", "private": true, - "description": "Chrome DevTools panel for inspecting Inertia.js requests, props, routes, and page state.", + "description": "Browser DevTools panel for inspecting Inertia.js requests, props, routes, and page state.", "contributors": [ "Pascal Baljet " ], @@ -12,6 +12,7 @@ "inertiajs", "devtools", "chrome-extension", + "firefox-addon", "mv3", "developer-tools" ], @@ -28,13 +29,17 @@ "@lucide/vue": "^1.17.0" }, "scripts": { - "dev": "vite build --watch --mode development", - "build": "vite build", - "clean": "rm -rf dist", + "dev:chrome": "EXTENSION_TARGET=chrome vite build --watch --mode development", + "dev:firefox": "EXTENSION_TARGET=firefox vite build --watch --mode development", + "build:chrome": "EXTENSION_TARGET=chrome vite build", + "build:firefox": "EXTENSION_TARGET=firefox vite build", + "clean": "rm -rf dist-chrome dist-firefox", "type-check": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", - "test:e2e": "playwright test -c tests/e2e/playwright.config.ts", + "test:e2e": "pnpm build:chrome && pnpm build:firefox && playwright test -c tests/e2e/playwright.config.ts", + "test:e2e:chrome": "pnpm build:chrome && playwright test -c tests/e2e/playwright.config.ts --project=chrome", + "test:e2e:firefox": "pnpm build:firefox && playwright test -c tests/e2e/playwright.config.ts --project=firefox", "format": "oxfmt", "lint": "oxlint ." }, @@ -43,18 +48,17 @@ "@tailwindcss/vite": "^4.0.0", "@types/chrome": "^0.0.281", "@types/node": "^24.10.13", + "@types/selenium-webdriver": "^4.35.6", "@vitejs/plugin-vue": "^6.0.0", + "@vue/test-utils": "^2.4.11", + "happy-dom": "^20.11.2", "oxfmt": "^0.51.0", "oxlint": "^1.66.0", + "selenium-webdriver": "^4.46.0", "tailwindcss": "^4.0.0", "typescript": "^5.9.3", "vite": "^8.0.0", "vitest": "^4.1.2", "vue": "^3.5.13" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "@tailwindcss/oxide" - ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 227ba83..682bd01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,15 +24,27 @@ importers: '@types/node': specifier: ^24.10.13 version: 24.13.3 + '@types/selenium-webdriver': + specifier: ^4.35.6 + version: 4.35.6 '@vitejs/plugin-vue': specifier: ^6.0.0 version: 6.0.8(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))(vue@3.5.40(typescript@5.9.3)) + '@vue/test-utils': + specifier: ^2.4.11 + version: 2.4.11(@vue/compiler-dom@3.5.40)(@vue/server-renderer@3.5.40)(vue@3.5.40(typescript@5.9.3)) + happy-dom: + specifier: ^20.11.2 + version: 20.11.2 oxfmt: specifier: ^0.51.0 version: 0.51.0 oxlint: specifier: ^1.66.0 version: 1.75.0 + selenium-webdriver: + specifier: ^4.46.0 + version: 4.46.0 tailwindcss: specifier: ^4.0.0 version: 4.3.3 @@ -44,7 +56,7 @@ importers: version: 8.1.5(@types/node@24.13.3)(jiti@2.7.0) vitest: specifier: ^4.1.2 - version: 4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) + version: 4.1.10(@types/node@24.13.3)(happy-dom@20.11.2)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) vue: specifier: ^3.5.13 version: 3.5.40(typescript@5.9.3) @@ -68,6 +80,9 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@bazel/runfiles@6.5.0': + resolution: {integrity: sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==} + '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} @@ -77,6 +92,10 @@ packages: '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -104,6 +123,9 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@oxc-project/types@0.139.0': resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} @@ -351,6 +373,10 @@ packages: cpu: [x64] os: [win32] + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@playwright/test@1.58.2': resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} engines: {node: '>=18'} @@ -578,6 +604,15 @@ packages: '@types/node@24.13.3': resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + '@types/selenium-webdriver@4.35.6': + resolution: {integrity: sha512-8nfyMRi4VvkY9QrQGyY/zkleAhnjnmE8YtdEeoCrWe3izp1P9vo9f5VTNRYF0up+l+kn+VuZah+je+bLddNV+g==} + + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@vitejs/plugin-vue@6.0.8': resolution: {integrity: sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==} engines: {node: ^20.19.0 || >=22.12.0} @@ -641,17 +676,78 @@ packages: '@vue/shared@3.5.40': resolution: {integrity: sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==} + '@vue/test-utils@2.4.11': + resolution: {integrity: sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==} + peerDependencies: + '@vue/compiler-dom': 3.x + '@vue/server-renderer': 3.x + vue: 3.x + peerDependenciesMeta: + '@vue/server-renderer': + optional: true + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + + buffer-image-size@0.6.4: + resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} + engines: {node: '>=4.0'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -659,6 +755,20 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enhanced-resolve@5.24.3: resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} engines: {node: '>=10.13.0'} @@ -689,6 +799,10 @@ packages: picomatch: optional: true + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -699,13 +813,58 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + happy-dom@20.11.2: + resolution: {integrity: sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw==} + engines: {node: '>=20.0.0'} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -854,14 +1013,30 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + nanoid@3.3.16: resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + obug@2.1.4: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} @@ -889,6 +1064,20 @@ packages: vite-plus: optional: true + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -913,14 +1102,50 @@ packages: resolution: {integrity: sha512-v4sDNP3fdNiWMfabO7OwOQdOX8TiQSztKyT1Wj0w+j7LDallJThJRBBBmzVGyYj0crMh7jlV4zepPkiNu9UwDQ==} engines: {node: ^10 || ^12 || >=14} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + rolldown@1.1.5: resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + selenium-webdriver@4.46.0: + resolution: {integrity: sha512-UlTkgnx9y+bf3QxFsrgUyMqC2oy1Yf+qcOs7xIC/XfsUPv/ow7Sicx6SJ7AeOn2/Z+xF1M8SLqyn983wipI82w==} + engines: {node: '>= 20.0.0'} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -931,6 +1156,25 @@ packages: std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + tailwindcss@4.3.3: resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} @@ -957,6 +1201,10 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -968,6 +1216,9 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vite@8.1.5: resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1052,6 +1303,9 @@ packages: jsdom: optional: true + vue-component-type-helpers@3.3.9: + resolution: {integrity: sha512-3c/UfMe0SqyEfcGTyH7mfshHagJ9QTCbppCb0/uGpHZpFug7+If3GeGZN7I0YheKEExemx3xldQPoO7PQSOLQg==} + vue@3.5.40: resolution: {integrity: sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==} peerDependencies: @@ -1060,11 +1314,40 @@ packages: typescript: optional: true + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + ws@8.21.2: + resolution: {integrity: sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + snapshots: '@babel/helper-string-parser@7.29.7': {} @@ -1080,6 +1363,8 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@bazel/runfiles@6.5.0': {} + '@emnapi/core@1.11.1': dependencies: '@emnapi/wasi-threads': 1.2.2 @@ -1096,6 +1381,15 @@ snapshots: tslib: 2.8.1 optional: true + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1126,6 +1420,8 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@one-ini/wasm@0.1.1': {} + '@oxc-project/types@0.139.0': {} '@oxfmt/binding-android-arm-eabi@0.51.0': @@ -1242,6 +1538,9 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.75.0': optional: true + '@pkgjs/parseargs@0.11.0': + optional: true + '@playwright/test@1.58.2': dependencies: playwright: 1.58.2 @@ -1398,6 +1697,17 @@ snapshots: dependencies: undici-types: 7.18.2 + '@types/selenium-webdriver@4.35.6': + dependencies: + '@types/node': 24.13.3 + '@types/ws': 8.18.1 + + '@types/whatwg-mimetype@3.0.2': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.13.3 + '@vitejs/plugin-vue@6.0.8(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))(vue@3.5.40(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.1 @@ -1499,16 +1809,81 @@ snapshots: '@vue/shared@3.5.40': {} + '@vue/test-utils@2.4.11(@vue/compiler-dom@3.5.40)(@vue/server-renderer@3.5.40)(vue@3.5.40(typescript@5.9.3))': + dependencies: + '@vue/compiler-dom': 3.5.40 + js-beautify: 1.15.4 + vue: 3.5.40(typescript@5.9.3) + vue-component-type-helpers: 3.3.9 + optionalDependencies: + '@vue/server-renderer': 3.5.40 + + abbrev@2.0.0: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + assertion-error@2.0.1: {} + balanced-match@1.0.2: {} + + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + + buffer-image-size@0.6.4: + dependencies: + '@types/node': 24.13.3 + chai@6.2.2: {} + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@10.0.1: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + convert-source-map@2.0.0: {} + core-util-is@1.0.3: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + csstype@3.2.3: {} detect-libc@2.1.2: {} + eastasianwidth@0.2.0: {} + + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.9 + semver: 7.8.5 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + enhanced-resolve@5.24.3: dependencies: graceful-fs: 4.2.11 @@ -1530,16 +1905,82 @@ snapshots: optionalDependencies: picomatch: 4.0.5 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fsevents@2.3.2: optional: true fsevents@2.3.3: optional: true + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + graceful-fs@4.2.11: {} + happy-dom@20.11.2: + dependencies: + '@types/node': 24.13.3 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + buffer-image-size: 0.6.4 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.21.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + immediate@3.0.6: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + is-fullwidth-code-point@3.0.0: {} + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jiti@2.7.0: {} + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.5.0 + js-cookie: 3.0.8 + nopt: 7.2.1 + + js-cookie@3.0.8: {} + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.32.0: optional: true @@ -1638,12 +2079,24 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 + lru-cache@10.4.3: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + + minipass@7.1.3: {} + nanoid@3.3.16: {} + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + obug@2.1.4: {} oxfmt@0.51.0: @@ -1692,6 +2145,17 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.75.0 '@oxlint/binding-win32-x64-msvc': 1.75.0 + package-json-from-dist@1.0.1: {} + + pako@1.0.11: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -1712,6 +2176,20 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + process-nextick-args@2.0.1: {} + + proto-list@1.2.4: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + rolldown@1.1.5: dependencies: '@oxc-project/types': 0.139.0 @@ -1733,14 +2211,62 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.1.5 '@rolldown/binding-win32-x64-msvc': 1.1.5 + safe-buffer@5.1.2: {} + + selenium-webdriver@4.46.0: + dependencies: + '@bazel/runfiles': 6.5.0 + jszip: 3.10.1 + tmp: 0.2.7 + ws: 8.21.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + semver@7.8.5: {} + + setimmediate@1.0.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} + source-map-js@1.2.1: {} stackback@0.0.2: {} std-env@4.2.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + tailwindcss@4.3.3: {} tapable@2.3.3: {} @@ -1758,6 +2284,8 @@ snapshots: tinyrainbow@3.1.0: {} + tmp@0.2.7: {} + tslib@2.8.1: optional: true @@ -1765,6 +2293,8 @@ snapshots: undici-types@7.18.2: {} + util-deprecate@1.0.2: {} + vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0): dependencies: lightningcss: 1.33.0 @@ -1777,7 +2307,7 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 - vitest@4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)): + vitest@4.1.10(@types/node@24.13.3)(happy-dom@20.11.2)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)): dependencies: '@vitest/expect': 4.1.10 '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) @@ -1801,9 +2331,12 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.3 + happy-dom: 20.11.2 transitivePeerDependencies: - msw + vue-component-type-helpers@3.3.9: {} + vue@3.5.40(typescript@5.9.3): dependencies: '@vue/compiler-dom': 3.5.40 @@ -1814,7 +2347,27 @@ snapshots: optionalDependencies: typescript: 5.9.3 + whatwg-mimetype@3.0.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + ws@8.21.2: {} diff --git a/release.sh b/release.sh index b661229..b697819 100755 --- a/release.sh +++ b/release.sh @@ -32,20 +32,18 @@ case $choice in *) echo "❌ Invalid choice. Exiting."; exit 1 ;; esac -# Bump package.json without tagging, then read back the resolved version +# Bump package.json without tagging, then read back the resolved version. Both manifests are +# generated from this version at build time (manifest.config.ts), so there is nothing to mirror. pnpm version "$RELEASE_TYPE" --no-git-tag-version >/dev/null NEW_VERSION=$(node -p "require('./package.json').version") TAG="v$NEW_VERSION" -# Chrome reads the version from manifest.json, so keep it in lockstep with package.json -node -e "const fs=require('fs');const m=require('./manifest.json');m.version='$NEW_VERSION';fs.writeFileSync('./manifest.json',JSON.stringify(m,null,2)+'\n')" - -# Re-format so the committed files already match oxfmt. Otherwise JSON.stringify expands the -# manifest arrays and CI's coding-standards job commits a follow-up "Fix code style" change. +# Re-format so the committed files already match oxfmt, otherwise CI's coding-standards job +# commits a follow-up "Fix code style" change. pnpm run format >/dev/null -# Commit and push the tag. Pushing the tag triggers the Release workflow, which builds, -# packages the zip, and publishes the GitHub release with the zip attached. +# Commit and push the tag. Pushing the tag triggers the Release workflow, which builds both +# targets and publishes the GitHub release with the Chrome and Firefox zips attached. git add -A git commit -m "$TAG" git tag -a "$TAG" -m "$TAG" @@ -55,4 +53,5 @@ git push --tags echo echo "✅ Tagged $TAG and pushed. CI will build, package, and publish the release." echo "🔗 https://github.com/inertiajs/inertia-devtools/actions" -echo "📦 When the Release workflow finishes, grab the zip from the release and upload it to the Chrome Web Store." +echo "📦 When the Release workflow finishes, upload the zips: the -chrome one to the Chrome Web Store," +echo " the -firefox one to addons.mozilla.org (see BROWSERS.md for the source-code upload it needs)." diff --git a/src/background.ts b/src/background.ts index 109d655..3a5ffcd 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,16 +1,14 @@ import { broadcastDevStatus, broadcastEntryPageState, broadcastRequestActive } from './background/broadcasts' import { handleCacheHit } from './background/cacheHit' -import { forgetProvenHosts, rememberProvenHost } from './background/hosts' +import { rememberProvenHost } from './background/hosts' import { ingestEntry } from './background/ingest' import { appendAndBroadcast } from './background/record' import { applyClientFlash, - clearAll, clearTabEntries, getDevActive, getEntries, getEvictedCount, - getOrigin, getPageStatesForTab, pairPageStateWithEntry, setDevActive, @@ -25,17 +23,9 @@ import type { ClientVisitSnapshot, ContentCacheHitMessage, ContentToBackgroundMessage, - DevToolsTestHooks, PageStateSnapshot, } from './types' -type DevToolsServiceWorkerGlobal = { - __inertiaDevtools: DevToolsTestHooks -} - -// E2E-only hook for tests/devtools/fixtures.ts. -declare const self: WorkerGlobalScope & DevToolsServiceWorkerGlobal - // Surface worker runtime errors in the worker's own console instead of failing silently. const workerScope = self as unknown as { addEventListener(type: 'error', listener: (event: ErrorEvent) => void): void @@ -88,30 +78,13 @@ browser.webRequest.onHeadersReceived.addListener( ['responseHeaders'], ) -self.__inertiaDevtools = { - getBuffer: (tabId) => getEntries(tabId), - getOrigin: (tabId) => getOrigin(tabId), - getPageStates: (tabId) => getPageStatesForTab(tabId), - clearAll: () => clearAll(), - ingest: (tabId, origin, id, basePath) => ingestEntry(tabId, origin, id, basePath), - forgetHosts: async () => { - await forgetProvenHosts() - await syncAllTabRules() - }, -} - function replyOk(sendResponse: (response: { ok: true }) => void): boolean { sendResponse({ ok: true }) return false } -/** - * Unlock the tab header for a host that just proved it runs the recorder. - * - * Its first response is therefore unstamped; every request made after the rule lands carries the - * tab UUID. - */ +/** Allow subsequent requests from a proven recorder host to carry the tab UUID. */ async function noteProvenHost(origin: string): Promise { if (await rememberProvenHost(origin)) { await syncAllTabRules() diff --git a/src/background/tabRules.ts b/src/background/tabRules.ts index f6dae0f..6cd27d4 100644 --- a/src/background/tabRules.ts +++ b/src/background/tabRules.ts @@ -3,6 +3,13 @@ import { DEVTOOLS_TAB_HEADER, TAB_STORAGE_KEY_PREFIX } from '../constants' import { getProvenHosts } from './hosts' import { clearTab, migrateTab } from './runtimeStore' +/** Use wire strings because Firefox does not expose Chrome's runtime enum objects. */ +const RULE_RESOURCE_TYPES = ['xmlhttprequest', 'main_frame', 'sub_frame'] as chrome.declarativeNetRequest.ResourceType[] + +const MODIFY_HEADERS = 'modifyHeaders' as chrome.declarativeNetRequest.RuleActionType + +const SET_HEADER = 'set' as chrome.declarativeNetRequest.HeaderOperation + function tabStorageKey(tabId: number): string { return `${TAB_STORAGE_KEY_PREFIX}${tabId}` } @@ -14,12 +21,7 @@ export async function readTabUuid(tabId: number): Promise { return typeof stored[key] === 'string' ? stored[key] : null } -/** - * Install the per-tab DNR rule that stamps outgoing requests with the stable tab UUID. - * - * The rule only covers hosts that have proven they run the recorder — `requestDomains` is the - * closest DNR gets to a host allowlist, so it widens to their subdomains too. - */ +/** Install a tab UUID rule for hosts that have proven recorder support. */ export async function writeDnrRule(tabId: number, uuid: string): Promise { const provenHosts = await getProvenHosts() @@ -41,18 +43,14 @@ export async function writeDnrRule(tabId: number, uuid: string): Promise { condition: { tabIds: [tabId], requestDomains: provenHosts, - resourceTypes: [ - chrome.declarativeNetRequest.ResourceType.XMLHTTPREQUEST, - chrome.declarativeNetRequest.ResourceType.MAIN_FRAME, - chrome.declarativeNetRequest.ResourceType.SUB_FRAME, - ], + resourceTypes: RULE_RESOURCE_TYPES, }, action: { - type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS, + type: MODIFY_HEADERS, requestHeaders: [ { header: DEVTOOLS_TAB_HEADER, - operation: chrome.declarativeNetRequest.HeaderOperation.SET, + operation: SET_HEADER, value: uuid, }, ], @@ -65,12 +63,6 @@ export async function writeDnrRule(tabId: number, uuid: string): Promise { } } -/** - * Ensure a tab has a stable UUID and matching header-injection rule before traffic starts. - * - * The rule is rewritten rather than assumed live: session rules die on browser restart, and the - * proven-host set they are scoped to grows as hosts reveal a recorder. - */ export async function ensureTabRule(tabId: number): Promise { const uuid = (await readTabUuid(tabId)) ?? crypto.randomUUID() diff --git a/src/browser.ts b/src/browser.ts index 5e7e3db..7407136 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1 +1,9 @@ export const browser: typeof chrome = (globalThis as { browser?: typeof chrome }).browser ?? globalThis.chrome + +/** + * Whether this browser strands the tab an app-scheme link (`vscode:`, `phpstorm:`) opens. + * + * Chrome hands the url to the OS handler and discards that tab. Firefox keeps it, blank, with + * no handle for the panel to close it by. `src/panel/lib/appScheme.ts` is the only reader. + */ +export const STRANDS_APP_SCHEME_TABS = 'browser' in globalThis diff --git a/src/devtools/devtools.html b/src/devtools.html similarity index 68% rename from src/devtools/devtools.html rename to src/devtools.html index 1ecadee..a844f9c 100644 --- a/src/devtools/devtools.html +++ b/src/devtools.html @@ -5,6 +5,6 @@ Inertia DevTools - + diff --git a/src/devtools/devtools.ts b/src/devtools/devtools.ts index 154b43d..0fb1d68 100644 --- a/src/devtools/devtools.ts +++ b/src/devtools/devtools.ts @@ -8,4 +8,7 @@ const tabId = browser.devtools.inspectedWindow.tabId // the query param; it is a single global key and races between multiple windows. browser.storage.session.set({ [SESSION_TAB_ID_KEY]: tabId }) -browser.devtools.panels.create('Inertia', '', `panel/panel.html?tabId=${tabId}`, () => {}) +// Firefox resolves panel assets from this page and rejects extension-absolute URLs. +// Keeping `devtools.html` at the build root makes the relative panel path work in both browsers. +// An empty icon lets Firefox use the manifest icon while Chrome renders the title. +browser.devtools.panels.create('Inertia', '', `panel/panel.html?tabId=${tabId}`) diff --git a/src/panel/App.vue b/src/panel/App.vue index 8eae577..59d8ccd 100644 --- a/src/panel/App.vue +++ b/src/panel/App.vue @@ -6,6 +6,7 @@ import EntryDetail from './components/EntryDetail.vue' import FilterBar from './components/FilterBar.vue' import ThemeToggle from './components/ThemeToggle.vue' import Timeline from './components/Timeline.vue' +import { useHostAccess } from './lib/useHostAccess' import { useResizableSidebar } from './lib/useResizableSidebar' import { useThemePreference } from './lib/useThemePreference' import { entriesStore } from './stores/entries' @@ -18,6 +19,8 @@ const ui = uiStore useThemePreference() +const hasHostAccess = useHostAccess() + // One dot conveys connection + activity, so a request starting or finishing never // reflows the toolbar the way an appearing/disappearing text label did. const status = computed(() => { @@ -128,6 +131,17 @@ async function onRetryHydration(): Promise { +
+ + The DevTools have no access to this site, so nothing can be recorded. Grant access from the extensions button in + the browser toolbar, then reload the page. + +
+
+import { APP_SCHEME_TARGET, launchAppScheme } from '../lib/appScheme' + +const props = defineProps<{ href: string }>() + +function open(event: MouseEvent): void { + if (launchAppScheme(props.href)) { + event.preventDefault() + } +} + + + diff --git a/src/panel/components/EditorLink.vue b/src/panel/components/EditorLink.vue index 692a2e9..e7f423c 100644 --- a/src/panel/components/EditorLink.vue +++ b/src/panel/components/EditorLink.vue @@ -3,6 +3,7 @@ import { ExternalLink } from '@lucide/vue' import { computed } from 'vue' import { buildEditorUrl } from '../lib/editors' import { uiStore } from '../stores/ui' +import AppSchemeAnchor from './AppSchemeAnchor.vue' const props = withDefaults( defineProps<{ @@ -45,11 +46,9 @@ const showPlainText = computed(() => Boolean(props.file) && ui.editor === 'off')