naiv-qr/CLAUDE.md
Ole-Morten Duesund 004b63f51c Add CLAUDE.md with build and architecture guidance
Documents the dcat issue workflow rule, bun commands, and the
non-obvious invariants future agents need: dist/ is the single source
of truth for what ships, the Firefox 142 floor and no-gecko.update_url
constraint for AMO-listed distribution, the pinned qrcode-generator
SHA and UTF-8 override, and the popup error-path conventions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:02:48 +02:00

4.4 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Workflow

  • Read AGENTS.md first. It defines the mandatory dcat (dogcat) issue-tracking workflow used here. The hard rule: before writing any code for a new bug, feature, or change, ask the user whether to create an issue first — no exceptions for "small" tasks. Run dcat prime after compaction, /clear, or starting a new session.
  • After committing, show a short summary of what was committed and why (per the user's global guidelines).

Commands

bun run build      # bundle popup.js + copy assets into dist/
bun run lint       # build, then web-ext lint (must be 0 errors / 0 warnings)
bun run package    # build, then produce a signable .zip in web-ext-artifacts/
bun run start      # build, then launch Firefox with the extension (auto-reload)

There is no test suite. bun run lint is the closest thing to CI — it must pass clean before publishing. The first invocation downloads web-ext via bunx; subsequent runs are fast.

Always use bun / bunx (never npm/npx/yarn).

Architecture

Naiv-QR is a Manifest V3 Firefox extension with no background script and no content script — just a toolbar action that opens a popup. When the user clicks the icon, popup/popup.js runs once, reads the active tab's URL via browser.tabs.query (allowed by activeTab only on user gesture), and renders an SVG QR code locally using the vendored qrcode-generator library. There are zero network requests.

dist/ is the only thing that ships

scripts/build.mjs is the single source of truth for what the extension contains. It:

  1. Wipes dist/.
  2. Runs Bun.build on popup/popup.js (esm, browser target, minified). The import qrcode from "../vendor/qrcode.js" line is what causes the vendored library to get inlined — there is no global qrcode at runtime.
  3. Copies manifest.json, popup/popup.html, popup/popup.css, icons/, and vendor/LICENSE.qrcode-generator verbatim into dist/.

web-ext-config.cjs points web-ext at dist/ only, so lint/package/run all operate on the build output. Adding a file to the repo does not put it in the extension — if a new asset (icon, locale, script) needs to ship, update scripts/build.mjs to copy it.

Compatibility & distribution constraints

  • manifest.json sets strict_min_version: 142.0. This floor exists because browser_specific_settings.gecko.data_collection_permissions was introduced in Firefox 140 desktop / 142 Android. Don't lower it without also removing that field.
  • Target distribution is AMO-listed. The manifest must not declare gecko.update_url — AMO handles auto-updates for listed extensions, and adding an update URL breaks the listed-channel review.
  • Permissions are deliberately minimal: activeTab (read current tab on click) and clipboardWrite (Copy URL button). Adding a new permission needs explicit justification — it's user-visible at install/upgrade and surfaces during AMO review.
  • data_collection_permissions declares "none". If any code is ever added that does collect data (telemetry, error reporting, etc.), that field must be updated honestly — it's surfaced to users during AMO review and at install time.

Vendored QR library

vendor/qrcode.js is kazuhikoarase/qrcode-generator, pinned to commit 83b7e8fe3fddd3b0368dbafd6ce56995bd25e3c8. AMO source-code review requires we point reviewers at this pinned upstream. If the vendored file is updated, update the pinned SHA in README.md and vendor/LICENSE.qrcode-generator if needed.

popup.js sets qrcode.stringToBytes = qrcode.stringToBytesFuncs["UTF-8"] because the library defaults to SJIS — without this, non-ASCII URLs would be misencoded.

Popup error paths

popup.js has three "can't render" branches that all route through showMessage():

  • browser.tabs.query throws → "Could not read the active tab."
  • URL protocol is in PRIVILEGED_PROTOCOLS (about:, moz-extension:, chrome:, view-source:, resource:, javascript:, data:) → "This page can't be shared."
  • qrcode-generator throws because the URL exceeds the largest QR version (~2,953 bytes for EC level L; we use M ≈ ~2,331) → "URL is too long to encode."

When adding new error states, keep them on this same showMessage() path so the popup never ends up in an inconsistent half-rendered state.