Skip to content

Analyzer

magicelk235 edited this page Aug 20, 2026 · 3 revisions

Analyzer

The analyzer is viaduct-cli's compatibility engine. Before (or instead of) converting a Chrome extension, it scans the manifest and every JS/CSS/HTML file for things Safari does differently, and produces a list of typed issues: an error/warning/info verdict, a human report (CONVERSION_REPORT.md), and a machine-readable JSON payload. It runs automatically inside every conversion and standalone via --analyze.

It lives in two functions:

Function File Scope
analyzeManifest(m) src/manifest/manifest.ts The parsed manifest.json, permissions, CSP, version, commands, content-script config. Returns { issues, permissionsToRemove }.
scanExtension(extPath, manifest, platforms) src/analyze/analyze.ts The files on disk, walks the package once and greps source for unsupported chrome.* calls, hardcoded URLs, icons, _locales, etc. Returns Issue[].

--analyze (in cli.tsanalyzeOnly) runs both and concatenates: [...mIssues, ...scanExtension(...)].

See Manifest Transform for the boundary: the analyzer only reports; transformManifest is what actually rewrites the manifest. --analyze previews those rewrites (via summarizeManifestChanges) without writing anything.


The issue model

Every finding is an Issue (src/types.ts):

type Severity = "error" | "warning" | "info";

interface Issue {
  severity: Severity;
  category: string;   // "api" | "manifest" | "permission" | "icons" | "i18n" | "ui" | "background" | ...
  message: string;    // what's wrong, in author-facing terms
  file?: string;      // "manifest.json" or a source path relative to the extension root
  line?: number;      // 1-based, for source hits
  fix?: string;       // the remediation shown after the message
  autoFixed?: boolean; // the converter handles this itself → non-blocking
  shimmed?: boolean;   // Safari rejects it, but the runtime shim emulates it → still works
}

Severity and the blocking gate

The three levels are not just cosmetic, error blocks conversion. The gate is a single function used everywhere so the terminal verdict, the Markdown report, and the process exit code can never disagree (src/analyze/report.ts):

export function countBlocking(issues, strict = false): number {
  return issues.filter(
    (i) => !i.autoFixed && (i.severity === "error" || (strict && i.severity === "warning"))
  ).length;
}
Level Meaning Blocks conversion?
error Safari (or the App Store, or the Xcode build) will reject the extension outright. Yes: conversion aborts unless --force. An autoFixed error does not block.
warning A feature degrades or misbehaves, but the extension still installs and loads. No (unless --strict, which promotes warnings to blocking for CI).
info FYI, or something the converter already handled. No.

Because error aborts the whole run, mis-classifying a "degrades but still works" case as error doesn't just flag one feature, it kills the entire extension. This is the single most important design constraint in the analyzer, and it's why blocking webRequest is a warning, not an error:

Blocking webRequest can't block in Safari, but the listener still registers and fires as an observer, and every other feature of the extension keeps working. Flagging it as error aborted conversion for essentially every ad-blocker, password manager, and VPN. Downgrading to warning lets the extension install; the network-blocking feature degrades and DNR migration is the real fix. (aa89a20)

[auto-fixed] and [shimmed], the tags users see

Two booleans give the author reassurance instead of alarm, and surface as badges in the report:

  • [auto-fixed]: the manifest rewrite handles it. The finding exists so you know it happened (e.g. an MV2 persistent background set to persistent: false, a dropped key/update_url, a version clamped to Apple's limits). autoFixed also removes the issue from the blocking count.
  • [shimmed]: Safari rejects the API/permission, but the Runtime Shim emulates it, so the call won't throw. Shown as _(shimmed, handled at runtime)_ in Markdown. (30509f8)

The two counts are disjoint: a shimmed issue is never also counted as auto-fixed, and the [shimmed] badge wins when both could apply:

if (i.shimmed) shimmed++;
else if (i.autoFixed) autoFixed++;

Both printIssues and the JSON payload carry separate autoFixed / shimmed totals.


What gets checked

The analyzer's checks fall into manifest-level (analyzeManifest) and source-level (scanExtension / scanJsContent). The source scan reads each file exactly once; several "one note is enough" checks (favicon, hardcoded chrome-extension://, chrome://settings) fire at most once per extension.

Master table

Check (real function) Sev Safari reason Remediation Commit
Unsupported chrome.* API (scanJsContent, from UNSUPPORTED_APIS) per-API (error/warning/info) API absent or behaves differently in Safari WebKit. Per-API fix from compat-data; some are shimmed. 24424b4, 744822b
Hardcoded chrome-extension://<id>/ URL (HARDCODED_EXT_URL_RE, JS/CSS/HTML) warning Safari assigns a random per-install origin; a baked-in 32-char id never resolves (resource 404s). Build URLs with chrome.runtime.getURL(path). 24424b4
Non-PNG declared icon (scanExtension icon loop) warning Safari's toolbar/store pipeline only renders PNG; .svg/.webp/.jpg shows a blank glyph and the App Store rejects it. Convert to PNG, update the manifest path. 24424b4
Missing declared icon (icon loop) error File referenced in icons/default_icon isn't in the package → Safari fails to load the extension. Add the file or remove the reference. 0d9e8e4
Missing manifest-referenced file (content scripts, background, popup, options, devtools, sandbox) error Safari silently drops the missing script/page. Add the file or remove the reference. 0d9e8e4
content_scripts[].world:"MAIN" (analyzeManifest) warning Supported only on Safari 18.4+; on older versions the script silently doesn't run and never has chrome.* access. Provide an ISOLATED-world fallback or feature-detect. 0d9e8e4
Missing App Store description (analyzeManifest) info The App Store requires a description for submission; Safari shows it in Settings → Extensions. Add a short description. 24424b4
Keyboard shortcut: no primary modifier (analyzeCommands) warning A chord with no Ctrl/Command/Alt (Shift-only counts as none) is silently dropped, the command gets no shortcut. Use e.g. Command+Shift+Y. 24424b4
Keyboard shortcut: ChromeOS-only modifier (Search) (analyzeCommands) warning Safari recognizes Ctrl/Command/Alt/Option/MacCtrl/Shift only; ChromeOS's Search has no equivalent. Use a Safari-supported modifier. 24424b4
>4 commands with a shortcut (analyzeCommands) warning [auto-fixed] Safari rejects the whole manifest on a 5th suggested_key; the transform strips the default chord from the extras. Extras still bindable in Safari → Settings → Extensions. 0d9e8e4
_locales present, no default_locale (scanExtension) error Chrome and Safari reject the load. Add default_locale. 744822b
default_locale has no / invalid messages.json (scanExtension) error Opaque load failure. Create/fix the default locale's messages.json. 744822b
Unresolvable __MSG_*__ in name/description (scanExtension, strict resolver) error Chrome localizes manifest fields from default_locale only; an unresolved key renders the literal __MSG_appName__ as the name, and the App Store rejects a placeholder name. Add the key to the default locale, or use a plain string. 744822b
URL match pattern left in permissions (MV3) (analyzeManifest) warning Legal MV2, but under MV3 a host pattern in permissions is silently ignored, grants no host access. Move it to host_permissions. 0d9e8e4, 1156251
Invalid content-script matches pattern (analyzeManifest) error Safari drops the entire content script for one bad pattern. Correct or remove the pattern. 24424b4
Invalid host_permissions pattern (analyzeManifest) warning [auto-fixed] Chrome-only schemes (ws:///wss://) can't be granted; Safari gives no host access. Transform drops the bad entry; the rest still apply. (handled) 0d9e8e4
Missing / non-numeric / out-of-range version (analyzeManifest) warning / info ([auto-fixed] when truncated/clamped) Apple's CFBundleShortVersionString needs ≤3 dot-separated integers each ≤65535; a suffix like -rc1 fails the Xcode build. Missing version fails the build. Use a numeric dotted version (1.2.3). 24424b4
Native messaging, connectNative/sendNativeMessage (source, from UNSUPPORTED_APIS) info [shimmed] No native-messaging-hosts manifest in Safari; the container app runs a loopback broker that launches the Chrome host and relays stdio. Keep the container app running; install the companion host app. a9c2e80
nativeMessaging permission (analyzeManifest) warning Surfaced from the permission too, native calls hide in minified bundles the source scan attributes imprecisely. Route to the app's SafariWebExtensionHandler. a9c2e80
navigator.userAgent Chrome-version sniff (UA_CHROME_SNIFF_RE) warning Safari's UA has no Chrome/ token; the version capture returns null and the dependent feature dies. The shim spoofs a token only in content scripts on http(s) pages, popup/options/background sniffs stay truthful and still fail. Remove the UA dependency. a534d69
Blocking webRequest (per-file) (BLOCKING_WEBREQUEST_RE + "blocking" literal) warning [shimmed] Safari ignores the blocking return; listeners still fire as observers. The extension loads; the network-modifying feature degrades. Migrate to declarativeNetRequest. aa89a20
Blocking-webRequest content blocker (class-level) (scanExtension) error For an ad/tracker blocker, blocking is the product, an extension that installs but blocks nothing is a misleading failure. Gated narrowly: needs webRequestBlocking + a broad host (<all_urls> / scheme wildcard) and no DNR, so password managers don't trip it. Convert the extension's DNR build instead (e.g. uBO Lite). dbe3442
Hardcoded chrome://extensions/shortcuts or chrome://settings link (CHROME_SETTINGS_URL_RE) warning No such page in Safari; the navigation errors. The shim swallows it, but the author still needs their own "edit in Safari → Settings → Extensions" affordance. Show the settings hint instead of linking the chrome:// page. f74597a
chrome://favicon / _favicon access (FAVICON_RE) warning No Safari equivalent. Fetch favicons directly, or drop the favicon UI. 744822b
CSP 'unsafe-eval' (analyzeManifest) warning Safari rejects eval in extension contexts regardless ('wasm-unsafe-eval' is fine and not flagged). Remove eval()/new Function. 24424b4
CSP remote script-src origin (analyzeManifest) warning Safari forbids remote script in extension pages; App Store review rejects it. Bundle the script locally. 24424b4, 1156251
background.type:"module" (MV3) (analyzeManifest) warning [auto-fixed] Causes silent popup failures on Safari/TestFlight. Stripped unless --keep-module. (handled) 24424b4
web_accessible_resources use_dynamic_url (analyzeManifest) warning [auto-fixed] Chrome-only; getURL() returns an unservable URL and the resource silently fails to load. (handled, flag cleared) 0d9e8e4
externally_connectable.ids (analyzeManifest) warning ID-based connections don't resolve in Safari. Use matches for web-page messaging. 24424b4
chrome_url_overrides (newtab / other) (analyzeManifest) warning Inconsistent/unsupported per platform. Test on macOS/iOS or drop it. 24424b4
Background setTimeout/setInterval (scanJsContent, background files) warning Unreliable in suspended Safari background contexts. Use chrome.alarms; persist to storage.local. 744822b
importScripts() in the MV3 service worker (scanJsContent) info (static) / warning (dynamic) The SW becomes a module background page where importScripts is undefined; static literals are hoisted into background.html, dynamic args can't be. (Webpack chunk loaders are recognized and stay info.) Replace dynamic importScripts(expr) with static ES imports. 744822b
tabs.connect (Safari 18 port bug) (CONNECT_RE) warning Safari 18 breaks iframe ↔ content-script ports. Use contentWindow.postMessage + runtime.sendMessage. ,
storage permission (analyzeManifest) info storage.sync doesn't sync across iCloud devices (maps to local). Implement custom cloud sync if needed. ,
iOS/iPadOS target caveats (scanExtension, when platforms includes iOS) info Responsive UI required; some APIs (contextMenus, notifications, downloads, cookies) are gated; App Store distribution only. Feature-detect per platform; test on iOS. 0d9e8e4

No-false-positive guard

Over-flagging is as harmful as under-flagging, a spurious error aborts a perfectly convertible extension. The scanner is deliberately conservative about what counts as a real hit (2cc6c34):

  • Blocking webRequest requires the literal quoted "blocking" token in the extraInfoSpec, not the bare word (which appears in comments, CSS classes, and unrelated identifiers).
  • Hardcoded extension URLs match only the exact 32-char [a-p]{32} id form, so chrome-extension:// joined with a variable isn't flagged.
  • Icon format is sniffed off the basename, so a dot in a directory name (assets.v2/icon48) isn't read as a bogus extension.
  • Native importScripts checks run only against the manifest-declared service worker; importScripts in a genuine web worker keeps working in Safari and is left alone. Args are extracted with the same balanced-paren scan the converter uses, so webpack's importScripts(o.p+o.u(t)) isn't truncated at the first inner ).
  • A more specific API hit (chrome.identity.launchWebAuthFlow) subsumes the generic namespace hit (chrome.identity) so you don't see the duplicate.
  • The scan skips its own output dir (staged_extension), node_modules, .git, and __MACOSX.

The scan walks the raw manifest and source, before the converter injects its own shim/polyfill/bridge scripts, so those injected files never produce false "unsupported API" hits.


Output: --analyze, --json, --report

--analyze runs the analyzer standalone (no conversion, no Xcode project) and returns exit code 1 when there are blocking issues, 0 otherwise, the same gate a real conversion uses. --strict promotes warnings to blocking. See CLI Reference for all flags.

Terminal (--analyze)

printIssues groups findings error → warning → info, prints each with its [category], location, [shimmed]/[auto-fixed] badge, message, and fix, then a tally line and the verdict:

Convertible: no blocking issues.
3 blocking issue(s) (--strict: warnings count as blocking).

Below the issues it also previews the manifest rewrites the converter would apply (from summarizeManifestChanges).

JSON (--analyze --json)

A single object per extension (batch + --json is rejected, one extension at a time for parseable output). The convertible field matches the real conversion gate exactly: blocking === 0, where blocking already excludes autoFixed issues, so a CI consumer can trust it. Built in cli.tsanalyzeOnly. Only these fields exist:

{
  "name": "My Extension",
  "appName": "My Extension",
  "bundleId": "com.viaduct.my-extension",
  "version": "1.2.3",
  "manifestVersion": 3,
  "platforms": "all",
  "counts": { "error": 0, "warning": 2, "info": 3 },
  "autoFixed": 2,
  "shimmed": 1,
  "blocking": 0,
  "convertible": true,
  "removedPermissions": ["idle", "contextMenus"],
  "manifestChanges": [
    "Version `1.2` → `1.2.0` (Apple format).",
    "Background made non-persistent (MV2 → Safari)."
  ],
  "issues": [
    {
      "severity": "warning",
      "category": "api",
      "message": "Blocking webRequest detected; Safari ignores the blocking return...",
      "file": "background.js",
      "line": 42,
      "fix": "Migrate the blocking rules to declarativeNetRequest rulesets...",
      "shimmed": true
    }
  ]
}

On a corrupt archive or missing manifest, JSON mode still emits parseable output rather than a stack trace: { "error": "<message>", "convertible": false }.

Report file (--report <file>)

Writes the report to disk. With --json it writes the JSON payload; otherwise it writes the Markdown report (buildReportMarkdown), the same document writeReportFile drops as CONVERSION_REPORT.md next to a real conversion's output, so the issue list survives past terminal scrollback (useful for CI logs / handoff). The Markdown report leads with a Status: line, per-severity counts, a Removed permissions section, a Manifest changes section, then the grouped issues with _(shimmed …)_ / _(auto-fixed)_ tags.


Related

  • Manifest Transform: the counterpart that actually applies the rewrites the analyzer previews.
  • Runtime Shim: what backs every [shimmed] finding (UA spoof, native-messaging broker, storage sync→local, dead-navigation swallowing).
  • Safari Quirks: the underlying WebKit/Safari behaviors these checks defend against (per-install origin, world:"MAIN" on 18.4+, Safari 18 port bug, PNG-only icons).
  • CLI Reference: --analyze, --json, --report, --strict, --force.
  • Limitations and FAQ: what can't be shimmed at all (blocking webRequest network blocking, favicon access).

Clone this wiki locally