Skip to content

Latest commit

 

History

History
210 lines (143 loc) · 15.2 KB

chrome-extension.md

File metadata and controls

210 lines (143 loc) · 15.2 KB
title Build the extension
description Build, load and package the Chrome DevTools extension.
A thin Manifest V3 shell around the devtools UI. Build it, load it unpacked, and zip it for the Chrome Web Store.

Build the extension

The Chrome extension lives in extension/. It detects Angular pages, creates the panel, and loads the devtools UI from extension/ui.

Files

extension/
  manifest.json          # Manifest V3, loopback host permissions, optional access to other hosts
  devtools.html
  devtools.js            # Checks the inspected page for Angular, creates the panel and the Elements pane
  frames.js              # Finds Angular in the top frame or one of its frames
  panel.html             # The panel page and its status view
  panel-actions.js       # Answers the UI's reveal and open-source requests
  connection-probe.js    # Looks for the devframe connection file on an origin
  panel-bridge.js        # Asks for host access, finds the dev server, connects the UI to it
  popup.html, popup.js   # The toolbar popup: Angular, build, server and overlay on the active tab
  options.html, options.js # The site access page: granted hosts with Remove
  extension-page.css     # Styles of the popup and the site access page
  icons/
  ui/                    # The built devtools UI (committed)

What the manifest asks for

Key Value
permissions activeTab and scripting, for the toolbar popup's one read of the clicked tab. Neither warns on install.
host_permissions *.localhost, 127.0.0.1 and [::1], over HTTP and HTTPS. *.localhost also matches localhost.
optional_host_permissions http://*/* and https://*/*. The panel requests one host at a time, only when you click Allow access.
minimum_chrome_version 111.
key The public key that fixes the extension ID to dcogniffeelebaolkkfbopmjcblhblfk.

Extension ID

Chrome derives the ID of an extension from its public key. The key in manifest.json gives every build the ID dcogniffeelebaolkkfbopmjcblhblfk, whether you load it unpacked or install it from the store. The Vite plugin and the Express hub trust chrome-extension://dcogniffeelebaolkkfbopmjcblhblfk by default, through PANGULAR_EXTENSION_IDS in packages/devtools/src/extension-origin.ts.

The maintainers keep the matching private key for the Chrome Web Store upload. It is not in the repository, and you never need it to build or load the extension. If you change the key, the ID changes too, and the server refuses the extension until its origin is in allowedOrigins or its ID is in PANGULAR_EXTENSION_IDS.

Build

pnpm extension:build

This builds the devtools UI (pnpm devtools:build), then replaces extension/ui with a copy of dist/devtools-ui.

extension/ui is committed. If you change app/, run pnpm extension:build and commit the result. CI builds the extension and fails when extension/ui is stale.

If you change panel.html, panel-bridge.js or a status message, run pnpm test:axe, then pnpm test:axe:extension. It loads panel.html with a stand-in for the chrome APIs and runs axe on each status the panel shows and on the connected panel. See Run the checks.

Load in Chrome

Go to chrome://extensions. Turn on the Developer mode toggle in the top right corner. Click Load unpacked and select the extension/ directory. Start a demo app and open DevTools. The Pangular Inspector panel appears once the extension detects Angular on the page.

After a rebuild, click the reload icon on the extension card, then reopen DevTools.

Build for Firefox

pnpm extension:build:firefox

This runs extension:build, then copies extension/ to dist/extension-firefox with a Firefox manifest. Nothing else in the copy changes. scripts/extension-firefox.mjs makes these changes to the manifest:

Change Why
Drops key and minimum_chrome_version Only Chrome reads them.
Adds browser_specific_settings.gecko Firefox needs the add-on ID (pangular-inspector@pangular-inspector.dev) and the oldest version, 140.
Declares data_collection_permissions: { required: ["none"] } Firefox asks every add-on what it collects. The extension sends data only to the devtools server.
Turns background.service_worker into scripts Firefox runs background scripts, not a service worker. The manifest has no background today.

dist/ is not committed, and CI does not build the Firefox copy. extension-firefox.test.ts checks the manifest it writes.

To try it, open about:debugging#/runtime/this-firefox, click Load Temporary Add-on and pick dist/extension-firefox/manifest.json. Then open the Firefox developer tools on an Angular app.

Firefox gives an extension a moz-extension:// origin with an ID of its own on each install, so the devtools server does not trust it the way it trusts the Chrome extension ID. If the panel says the server refused the request, add the origin it names to allowedOrigins.

Package for the Chrome Web Store

pnpm extension:zip

This runs extension:build, then writes dist/pangular-inspector-extension.zip. The zip leaves out .DS_Store files and drops key from the manifest, because the Chrome Web Store refuses a manifest with a key.

CI builds the same zip, without a key, on every pull request and push to main. Download it from the run's chrome-extension artifact to try a branch without building it.

For the first upload of a new store item, give the script the private key so the store keeps the ID dcogniffeelebaolkkfbopmjcblhblfk:

PANGULAR_EXTENSION_KEY=/path/to/pangular-inspector-extension-key.pem pnpm extension:zip

The key goes into the zip as key.pem, and the script stops if it doesn't match the key in the manifest. Later updates don't need it.

Upload

  1. Bump version in extension/manifest.json.
  2. Go to the Chrome Developer Dashboard.
  3. Click New item (or open the existing item) and upload the zip. For New item, build the zip with PANGULAR_EXTENSION_KEY set, as above.
  4. Fill in the listing details and submit for review.
The privacy policy for the listing is in docs/privacy-policy.html.

How the panel connects

Angular detection

The extension has no content scripts and no background worker. devtools.js runs only while DevTools is open, and checks the inspected page with chrome.devtools.inspectedWindow.eval(), which needs no host permission. It checks for an ng-version attribute or a window.ng global up to 10 times, 500 milliseconds apart, and again after each navigation. Each check asks the top frame first, then up to 20 frames from inspectedWindow.getResources() with eval(..., {frameURL}) (frames.js). It creates the panel and the Angular component sidebar pane the first time a check passes.

panel-bridge.js runs the same frame search after it reads the page origin. When Angular runs only in a frame, it keeps that frameURL for every later eval (page id, Elements selection, reveal and open source) and probes the frame's origin for the server.

The sidebar pane calls pane.setExpression() on every Elements selection with an expression that calls window.__pangularComponentDetail($0). The overlay sets that helper when inspectors.components is on; it answers with a summary of the masked component detail the panel gets (element-sidebar.ts).

Host access

panel-bridge.js reads the origin of the inspected page. If the page is not served over http or https, it stops and says so.

It then calls chrome.permissions.contains() for <scheme>://<hostname>/* of the page. Loopback hosts pass, since the manifest grants them. For any other host it shows the Allow access button. The button calls chrome.permissions.request() for that one pattern and, if Chrome grants it, starts over.

Finding the server

With access granted, it looks for the devtools server under these paths, in this order of preference:

  1. /__pangular/
  2. /__devframes/pangular/
  3. /__devframe/
  4. /

Under each path it fetches __devframe/__connection.json and __connection.json, with no credentials, no cache, no redirects and a 1.5 second timeout. It sends the first URL alone. If that URL fails, or has no answer after 150 milliseconds, it sends the other requests at once, so a missing server costs one timeout rather than one per URL. The first URL in the order above whose response is OK and parses as JSON wins.

It records the status of each request, or "no answer" when the request fails or times out. If none answers, the status view lists every URL it tried with its status and links to the setup section of the README. If any request got 401 or 403, the status view says the server refused the request, shows up to 200 characters of the response text and links to the 403 notes on the Vite page instead. Both views have a Try again button that starts the search over.

Waiting for the page id

The overlay sets window.__pangularPageId once it claims the page id, and removes it when it is disposed. After it finds the server, the panel evaluates that global every 250 milliseconds for up to five seconds. If the global never appears, it reads the pangular-page-id value from sessionStorage once, for overlays that do not set the global, and loads the UI with whatever it got.

Loading the UI

The panel loads ui/index.html with three query parameters:

Parameter Value
baseURL The path that served the connection file, on the origin of the page.
pageId The page id from Waiting for the page id, when there is one.
theme The DevTools theme name, dark or default.

Outside the extension, the UI accepts a baseURL only on its own origin. Inside the extension, it accepts any http or https URL. The panel only passes hosts the extension can reach.

The search starts as soon as the panel opens. On each navigation of the inspected page, the panel shows its status view again and repeats the whole search right away.

Theme

The panel follows the DevTools theme. panel-bridge.js reads chrome.devtools.panels.themeName at startup, sets data-theme on panel.html so the status view matches, and passes the name to the UI as the theme parameter (dark or default, which is light). Its setThemeChangeHandler updates panel.html and posts a pangular:theme-change message to the UI frame when DevTools switches theme. ThemeService in the panel sets data-theme on <html>, and the CSS tokens in app/src/styles/_theme.scss follow it.

Elements panel selection

The overlay defines window.__pangularComponentOf on the page. It takes an element and returns the id of the nearest component host, through shadow roots, or null.

When the Elements panel selection changes, panel-bridge.js evaluates it with $0. If it gets an id, it posts a pangular:inspect-component message to the UI frame. The UI accepts the message only from its parent window and its own origin, and only while the Components tab is open. The tab then expands the parent rows, clears the filter if needed, selects the row and scrolls it into view.

Reveal and open source

The overlay also defines window.__pangularHostOf(pageId, id) and window.__pangularClassOf(pageId, id). They return the host element or the component class of an instance id, and null when the id belongs to another page.

Inside the extension (a chrome-extension: frame), the Components detail header shows Reveal in Elements and Open source. The UI posts pangular:reveal-element or pangular:open-source with a requestId, the pageId and the instance id to its parent. panel-bridge.js accepts them only from the UI frame and its own origin, and panel-actions.js handles them:

Request What the extension does
pangular:reveal-element Evaluates inspect() on the host element in the inspected page.
pangular:open-source Looks up the file among the scripts in chrome.devtools.inspectedWindow.getResources() by path suffix, skipping style sheets and calls chrome.devtools.panels.openResource at line - 1. If no resource matches, it evaluates inspect() on the class.

It answers with pangular:panel-action-result, carrying the same requestId and ok. The UI gives up after 6 seconds.

Where to next