---
phase: 01-stabilize-video-pipeline
plan: 06
subsystem: build-pipeline
tags: [vite, crxjs, rollup-input, mv3, offscreen-document, dead-code-deletion, build-config, p0-1, audit-p2-17, audit-p2-18, d-07, d-08]

# Dependency graph
requires:
  - phase: 01-stabilize-video-pipeline
    provides: "Plan 03 created src/offscreen/index.html + src/offscreen/recorder.ts (the crxjs-managed entry that replaces the orphan offscreen/ dir); Plan 05 left chrome.runtime.getURL('offscreen/index.html') in src/background/index.ts:45 for Plan 06 to reconcile against the actual crxjs emit path"
provides:
  - "Collapsed vite.config.ts: 226 → 21 lines (-205). The 174-line inline copy-offscreen plugin (audit P0 #1 root cause) is GONE."
  - "Orphan offscreen/ top-level directory deleted (offscreen/index.ts + offscreen/index.html — both dead per D-08)."
  - "rollupOptions.input.offscreen wired to src/offscreen/index.html (RESEARCH.md Example B); crxjs picks up the recorder.ts module via the HTML <script> reference."
  - "Outcome A confirmed: crxjs PRESERVES the src/ prefix in the bundled output — dist/src/offscreen/index.html, NOT dist/offscreen/index.html."
  - "src/background/index.ts line 45 reconciled: chrome.runtime.getURL('src/offscreen/index.html') now matches the actual emit path (RESEARCH.md Pitfall 5 binding)."
  - "Clean dist/ layout: manifest.json + service-worker-loader.js + 7 hashed assets/*.js bundles + bundled HTML for offscreen and popup + icons."
affects: [01-07-ffprobe-gate]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "Minimal vite config pattern: crx() + rollupOptions.input — the canonical crxjs MV3 idiom (discussions #919 and #1060)."
    - "Build-time path binding: the SW's chrome.runtime.getURL string MUST equal the rollup input key path (Pitfall 5). Verified empirically post-build, not inferred."
    - "Build-config attack-surface reduction: deleting an inline plugin that called this.emitFile with a stringified JS payload removes a string-injection vector (T-1-NEW-06-01)."

key-files:
  created: []
  modified:
    - "vite.config.ts (226 → 21 lines; the entire inline copy-offscreen plugin block, the misplaced publicDir+copyPublicDir keys, and the manualChunks:undefined shape are all gone; replaced with RESEARCH.md Example B verbatim)"
    - "src/background/index.ts:45 (1-line change; getURL('offscreen/index.html') → getURL('src/offscreen/index.html') to match crxjs Outcome-A emit path)"
  deleted:
    - "offscreen/index.ts (60 lines of dead tabCapture-era recorder; replaced by src/offscreen/recorder.ts per D-07 / D-08)"
    - "offscreen/index.html (10 lines; replaced by src/offscreen/index.html created by Plan 03)"
    - "offscreen/ top-level directory (now empty; rmdir'd)"

key-decisions:
  - "Outcome A obtained: crxjs preserves the 'src/' prefix from the rollupOptions.input key. dist/src/offscreen/index.html is the emitted path, not dist/offscreen/index.html. RESEARCH.md Pitfall 5 documented both possibilities; the runtime verification settled it. SW URL string was updated accordingly (1-line fix in src/background/index.ts:45)."
  - "No path adjustment via 'leaving the SW URL unchanged' (Outcome B) was used — that would have required some non-crxjs build trick to strip the src/ prefix, and there's no benefit. Outcome A is the canonical crxjs behavior; matching the SW URL to it is the simplest correct path."
  - "Followed the plan's verbatim Example-B vite.config.ts (with double quotes per the plan body, which differ from the codebase's prior single-quote style — but the plan body specifies double quotes, and Prettier hasn't yet established a project-wide rule, so verbatim was preferred over style-rewrap)."
  - "Did NOT mark REQ-video-ring-buffer complete: per the prompt and Plan 05's precedent, Plan 07's ffprobe gate is the requirement-satisfaction proof."

patterns-established:
  - "crxjs entry pattern: HTML in rollupOptions.input → crxjs binds the HTML's <script type=module> ref → bundled output preserves the input key path. The SW resolves the runtime URL via chrome.runtime.getURL(<same-key>)."
  - "Two-step verification flow for any future build-config change: (1) edit vite.config.ts, (2) rm -rf dist && npm run build, (3) inspect dist/ for the actual emit paths, (4) reconcile any SW/manifest references that hard-code paths. This avoids the Pitfall 5 class of 'works locally because no one ran the build' bugs."

requirements-completed: []  # REQ-video-ring-buffer remains pending Plan 07 ffprobe gate

# Metrics
duration: 3min
completed: 2026-05-15
---

# Phase 01 Plan 06: Vite Config Collapse Summary

**Collapsed `vite.config.ts` from 226 lines to 21 by deleting the 174-line inline `copy-offscreen` plugin (audit P0 #1) and the dead `offscreen/` top-level directory; wired the new `src/offscreen/index.html` entry via `rollupOptions.input`; verified the crxjs emit path against the SW's `chrome.runtime.getURL(...)` string (Pitfall 5) and adjusted the SW URL to `src/offscreen/index.html` to match Outcome A.**

## Performance

- **Duration:** ~3 min
- **Started:** 2026-05-15T16:09:24Z
- **Completed:** 2026-05-15T16:11:30Z
- **Tasks:** 2 (vite.config.ts rewrite + dead-code deletion; build verification + path reconciliation)
- **Commits:** 2 (one per task)
- **Files modified:** 2 (`vite.config.ts`, `src/background/index.ts`)
- **Files deleted:** 2 (`offscreen/index.ts`, `offscreen/index.html`) + 1 dir (`offscreen/`)

## Before / After

| Snapshot | vite.config.ts lines | Notes |
|----------|----------------------|-------|
| Pre-Plan 06 | **226** | Inline copy-offscreen plugin (lines 13-216) + misplaced publicDir/copyPublicDir + manualChunks:undefined shape. |
| Post-Task 1 | **21** | RESEARCH.md Example B verbatim — crx() + rollupOptions.input.offscreen. The plan's `wc -l vite.config.ts ≤ 30` ceiling is comfortably met. |

```
$ find offscreen/ 2>&1 || echo absent
bfs: error: offscreen/: No such file or directory.
absent
```

## Task 1: vite.config.ts rewrite + offscreen/ deletion

**Commit:** `23e69d0 refactor(01-06): delete inline copy-offscreen plugin and orphan offscreen/ directory`

**Deletions:**
- `offscreen/index.ts` (60 lines — orphan dead-code from tabCapture era; D-08 explicit DELETE target; audit P2 #18)
- `offscreen/index.html` (10 lines — replaced by `src/offscreen/index.html` per D-07 / Plan 03)
- `offscreen/` directory (rmdir after the two file deletes; verified empty first)
- `vite.config.ts` inline `copy-offscreen` plugin (174 lines, lines 13-216): the `this.emitFile`-based emitter for both the offscreen HTML and the stringified JS module that ran the tabCapture-era chromeMediaSource + IndexedDB recording. T-1-NEW-06-01 mitigation.
- `vite.config.ts` misplaced `publicDir: 'public'` / `copyPublicDir: true` (audit P2 #17 — there is no `public/` directory in this repo)
- `vite.config.ts` `rollupOptions.output.manualChunks: undefined` (no longer needed; crxjs handles chunking)

**Replacement — final `vite.config.ts`:**

```typescript
import { defineConfig } from 'vite';
import { crx } from '@crxjs/vite-plugin';
import manifest from './manifest.json';

export default defineConfig({
  plugins: [
    crx({
      manifest,
      contentScripts: {
        injectCss: false,
      },
    }),
  ],
  build: {
    rollupOptions: {
      input: {
        offscreen: 'src/offscreen/index.html',
      },
    },
  },
});
```

**Acceptance gates (all PASS):**

| Gate | Expected | Actual |
|------|----------|--------|
| `test ! -f offscreen/index.ts` | exit 0 | PASS |
| `test ! -f offscreen/index.html` | exit 0 | PASS |
| `test ! -d offscreen` | exit 0 | PASS |
| `grep -c "copy-offscreen" vite.config.ts` | 0 | 0 |
| `grep -c "this.emitFile" vite.config.ts` | 0 | 0 (T-1-NEW-06-01 grep gate) |
| `grep -c "VideoRecorderDB" vite.config.ts` | 0 | 0 |
| `grep -c "openIndexedDB" vite.config.ts` | 0 | 0 |
| `grep -c "chromeMediaSource" vite.config.ts` | 0 | 0 |
| `grep -c "rollupOptions" vite.config.ts` | 1 | 1 |
| `grep -c "src/offscreen/index.html" vite.config.ts` | 1 | 1 |
| `wc -l vite.config.ts` | ≤ 30 | 21 |
| `npx tsc --noEmit` | exit 0 | PASS |
| `npx vitest run` | 9/9 PASS | 9/9 PASS |

## Task 2: Build verification + offscreen URL reconciliation

**Commit:** `6aeeda4 fix(01-06): align ensureOffscreen URL with crxjs emit path`

### `npm run build` output (excerpt)

```
> ai-call-extension@1.0.0 build
> tsc && vite build

vite v5.4.21 building for production...
transforming...
✓ 59 modules transformed.
rendering chunks...
computing gzip size...
dist/service-worker-loader.js                     0.04 kB
dist/icons/icon16.png                             0.08 kB
dist/icons/icon48.png                             0.12 kB
dist/icons/icon128.png                            0.31 kB
dist/assets/index.ts-loader-BkeIfRno.js           0.34 kB
dist/src/offscreen/index.html                     0.39 kB │ gzip:  0.23 kB
dist/src/popup/index.html                         0.76 kB │ gzip:  0.51 kB
dist/manifest.json                                1.14 kB │ gzip:  0.55 kB
dist/assets/index-BXJP9H5s.css                    1.08 kB │ gzip:  0.54 kB
dist/assets/modulepreload-polyfill-B5Qt9EMX.js    0.71 kB │ gzip:  0.40 kB
dist/assets/logger-D8-HRtHy.js                    1.04 kB │ gzip:  0.32 kB
dist/assets/index.html-BR0hR1fK.js                2.27 kB │ gzip:  1.04 kB
dist/assets/offscreen-SqX0ET3Q.js                 3.03 kB │ gzip:  1.42 kB
dist/assets/index.ts-BcLq5y3Q.js                 73.79 kB │ gzip: 24.04 kB
dist/assets/index.ts-D-8Ku9Ua.js                104.47 kB │ gzip: 32.91 kB
✓ built in 1.09s
```

### `find dist -type f | sort` — full layout for Plan 07's manual smoke load

```
dist/assets/index-BXJP9H5s.css
dist/assets/index.html-BR0hR1fK.js
dist/assets/index.ts-BcLq5y3Q.js
dist/assets/index.ts-D-8Ku9Ua.js
dist/assets/index.ts-loader-BkeIfRno.js
dist/assets/logger-D8-HRtHy.js
dist/assets/modulepreload-polyfill-B5Qt9EMX.js
dist/assets/offscreen-SqX0ET3Q.js
dist/icons/icon128.png
dist/icons/icon16.png
dist/icons/icon48.png
dist/manifest.json
dist/service-worker-loader.js
dist/src/offscreen/index.html
dist/src/popup/index.html
```

### Outcome identification — RESEARCH.md Pitfall 5

The bundled offscreen HTML lands at `dist/src/offscreen/index.html` (crxjs **preserved** the `src/` prefix from the rollup input key). `dist/offscreen/index.html` does NOT exist. This is **Outcome A** per the plan's dichotomy.

**SW URL adjustment:** Plan 05 left `chrome.runtime.getURL('offscreen/index.html')` at `src/background/index.ts:45`. Under Outcome A this would have produced `ERR_FILE_NOT_FOUND` at `chrome.offscreen.createDocument` time and broken Plan 07's manual smoke load. The one-line edit:

```diff
-    const url = chrome.runtime.getURL('offscreen/index.html');
+    const url = chrome.runtime.getURL('src/offscreen/index.html');
```

The committed SW URL string and the actual `dist/` emit path now match — verified empirically, not inferred.

### `dist/manifest.json` permissions verification

```
$ node -e "const m=require('./dist/manifest.json'); const p=m.permissions; \
    console.log('Has desktopCapture:', p.includes('desktopCapture')); \
    console.log('Has tabCapture:', p.includes('tabCapture'));"

Has desktopCapture: true
Has tabCapture: false
```

The post-D-A6 manifest amendment propagated correctly through crxjs into the bundled `dist/manifest.json` — `desktopCapture` present, `tabCapture` absent, `alarms` absent (already removed by Plan 05's manifest cleanup).

### Acceptance gates (all PASS)

| Gate | Expected | Actual |
|------|----------|--------|
| `npm run build` exit code | 0 | 0 |
| `dist/manifest.json` exists | YES | YES |
| `dist/src/offscreen/index.html` OR `dist/offscreen/index.html` | one of two | dist/src/offscreen/index.html (Outcome A) |
| `dist/manifest.json` permissions contains `desktopCapture` | YES | YES |
| `dist/manifest.json` permissions contains `tabCapture` | NO | NO |
| SW `chrome.runtime.getURL(...)` string matches dist emit | YES | YES (src/offscreen/index.html) |
| `dist/assets/` JS file count | ≥ 1 | 7 |
| `npx tsc --noEmit` post-edit | exit 0 | PASS |
| `npx vitest run` post-edit | 9/9 PASS | 9/9 PASS |

## Deviations from Plan

### Rule 1 — Bug fix (auto-applied, paired with Task 2)

**1. [Rule 1 - Bug] SW URL `chrome.runtime.getURL('offscreen/index.html')` would have 404'd against the Outcome-A dist layout**

- **Found during:** Task 2 (the planned post-build path reconciliation — this is the exact scenario RESEARCH.md Pitfall 5 calls out).
- **Issue:** Plan 05's executor left the pre-amendment string `'offscreen/index.html'` at `src/background/index.ts:45`. Once Plan 06 wired `rollupOptions.input.offscreen = 'src/offscreen/index.html'` and crxjs preserved the `src/` prefix in the emit, the SW URL no longer pointed at a real file. `chrome.offscreen.createDocument` would have returned `ERR_FILE_NOT_FOUND`. Plan 05's executor explicitly flagged this as the Task-2 runtime verification of Plan 06 (citation: STATE.md decisions block).
- **Fix:** One-line edit — `'offscreen/index.html'` → `'src/offscreen/index.html'`.
- **Files modified:** `src/background/index.ts:45`.
- **Verification:** Post-edit `npm run build` still exits 0; the committed string is `chrome.runtime.getURL('src/offscreen/index.html')` and `dist/src/offscreen/index.html` exists.
- **Committed in:** `6aeeda4` (Task 2 commit).

This was foreseen by the plan body itself (it explicitly describes the A/B outcome dichotomy and prescribes the edit conditional on which outcome obtains). It is a deviation only against the literal "Task 2 may need no commit if no path adjustment was needed" branch — Outcome A required the adjustment, so Task 2 produced its own commit. Not a true deviation in spirit; logged here for traceability.

---

**Total deviations:** 1 auto-fixed (Rule 1 — path binding).
**Impact on plan:** Zero scope creep. The plan body anticipated exactly this fix and put it inline as Task 2 step (4). The line edit was load-bearing for Plan 07's manual smoke load.

## Issues Encountered

None. Build was clean on first run. Tests stayed at 9/9 throughout. No tsc errors. No regressions in Plan 04's port-based test suite or Plan 02 / Plan 03's ring-buffer + codec-check suites.

## TDD Gate Compliance

This plan was `type: execute` (not `type: tdd`). No RED/GREEN/REFACTOR cycle required. The existing test suite served as a regression guard — `npx vitest run` returned 9/9 PASS both at the start of the plan and after each task commit.

## Authentication Gates

None. Pure build-pipeline refactor.

## Known Stubs

None. The replacement `vite.config.ts` is complete and functional — `npm run build` produces a loadable extension. No placeholder/TODO/coming-soon markers introduced.

## Threat Flags

None. Threat surface is REDUCED, not expanded:
- T-1-NEW-06-01 (string-injected code via inline plugin): MITIGATED — the entire inline plugin with its `this.emitFile({ source: \`<JS-as-string>\` })` calls is deleted. Grep gate `grep -c "this.emitFile" vite.config.ts` returns 0.
- T-1-NEW-06-02 (orphaned root-level offscreen): MITIGATED — `offscreen/` directory absent. The post-deletion `find offscreen/` reports "No such file or directory" / "absent".

## CLAUDE.md Compliance

- No `as any` casts introduced in any modified file (vite.config.ts has no casts; the SW edit is a string literal change).
- No `@ts-ignore` introduced.
- `vite.config.ts` follows the canonical RESEARCH.md Example B form (lifted verbatim from a cited authoritative source — crxjs documentation + discussion #919).
- No new constants needed to be moved; the replacement file is small enough that no extracted-constants refactor applies.

## Next Plan Readiness — Plan 07

Plan 07 (the ffprobe acceptance gate per D-12) can now:
1. Load `dist/` unpacked into Chrome ≥ 116 (no further build needed; `dist/manifest.json` is the loadable extension).
2. Trigger the recording from popup; the offscreen at `dist/src/offscreen/index.html` will be created by the SW's `chrome.offscreen.createDocument({ url: 'chrome-extension://<id>/src/offscreen/index.html', ... })` call.
3. Run the ffprobe acceptance gate against the exported `last_30sec.webm`.

The full `dist/` listing is captured above so Plan 07's smoke test knows the exact files to expect.

> Plan 07 loads `dist/` unpacked into Chrome ≥ 116 and runs the ffprobe acceptance gate (D-12).

## Self-Check

Verified after writing this summary:
- vite.config.ts file exists: FOUND.
- src/background/index.ts file exists: FOUND.
- offscreen/ directory absent: FOUND (the absence itself was checked: `find offscreen/ 2>&1 || echo absent` → absent).
- dist/manifest.json exists: FOUND.
- dist/src/offscreen/index.html exists: FOUND.
- Commit `23e69d0` exists in git log (Task 1).
- Commit `6aeeda4` exists in git log (Task 2).
- `npx tsc --noEmit` exits 0.
- `npx vitest run` reports 9/9 PASS across 4 test files.
- `wc -l vite.config.ts` returns 21 (≤ 30 plan ceiling).
- `grep -c "copy-offscreen\|chromeMediaSource\|this.emitFile" vite.config.ts` returns 0.
- SW URL string `chrome.runtime.getURL('src/offscreen/index.html')` matches Outcome-A emit path `dist/src/offscreen/index.html`.

## Self-Check: PASSED
