Skip to content
This repository was archived by the owner on Aug 9, 2026. It is now read-only.

Commit 6f741de

Browse files
authored
build: unify MV2 and MV3 into single branch (#263)
1 parent 3b847ef commit 6f741de

12 files changed

Lines changed: 296 additions & 88 deletions

‎AGENTS.md‎

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
1-
# HyperChat Codex Workflow (MV3 Main)
1+
# HyperChat Codex Workflow
22

33
## Branch Discipline
44

5-
- Make code changes on `mv2` first.
6-
- `main` is the real MV3 branch.
7-
- `mv3` and `mv3-ltl` are retired. Do not use them as MV3 source branches for new work or cross-repo sync; `main` is the MV3 line LiveTL consumes.
8-
- Do not implement feature/fix work directly on `main`.
9-
- If a task starts on another branch, switch to `mv2` before editing unless the user explicitly asks otherwise.
5+
- `main` is the only maintained source branch. Make code changes there.
6+
- `mv3` and `mv3-ltl` are retired. The remote `mv2` branch is a temporary rollback reference until LiveTL migration and runtime validation finish; do not use it for new work or delete it early.
7+
- There is no branch ladder any more. MV2 and MV3 are **build targets on `main`**, not branches — see "Manifest Version Targets" below.
108
- If a task touches both HyperChat and LiveTL, HyperChat still goes first.
119
- Cross-repo order is mandatory:
12-
1. HyperChat `mv2`
13-
2. HyperChat `main`
14-
3. LiveTL `develop`
15-
4. LiveTL `mv3-fr`
16-
5. LiveTL `release`
10+
1. HyperChat `main`
11+
2. LiveTL `develop`
12+
3. LiveTL `mv3-fr`
13+
4. LiveTL `release`
1714
- Never start cross-repo work in LiveTL when the HyperChat submodule also needs to change.
1815
- If the task also requires syncing YtcFilter (YTCF), do it after HyperChat `main` is updated:
1916
- merge HyperChat `main` into YTCF `master`
@@ -28,17 +25,63 @@
2825
- `order matters`
2926
- Avoid padded scopes, issue-number prefixes, and changelog-style essays in commit subjects.
3027
- A slightly dry or funny commit is fine if it is still clear at a glance.
31-
- Prefer proper merges when carrying `mv2` work into `main`.
32-
- If MV3 needs follow-up adaptation, keep that as a small, explicit commit after the merge instead of rewriting history or hand-copying changes.
28+
29+
## Manifest Version Targets
30+
31+
- `main` builds three targets: `chrome` (MV3), `firefox` (MV3), and `mv2` (MV2, Firefox-only).
32+
- The `mv2` target is **not** legacy cruft. Firefox's MV3 support is unreliable for LiveTL's needs, so LiveTL's Firefox variant will consume it after the downstream migration. Do not "simplify" it away.
33+
- Keep the MV2/MV3 distinction in exactly two places:
34+
- `src/manifest.json` — `{{mv2}}.` / `{{mv3}}.` key prefixes, resolved by `scripts/resolve-manifest.ts`. The MV tag must come **first** in nested keys (`{{mv3}}.{{firefox}}.background`); the reverse order leaks a literal tag into the built manifest.
35+
- `__MV__` in source — a build-time constant, so unused branches are dropped per target.
36+
- Do not add per-MV source files (`chat-background.mv2.ts` and friends). `main`'s architecture — thin background plus the broker in `src/ts/messaging.ts` — runs under MV2 as-is. Port MV3 patterns down to MV2, never MV2's persistent-background design up.
37+
- Prefer shared, untagged config. Only tag a key when MV2 and MV3 genuinely differ.
38+
- Only the two MV3 zips are released. `mv2` is built in CI for breakage coverage and consumed by LiveTL via submodule.
39+
40+
## Build-Time Constants (submodule consumers)
41+
42+
`src/` depends on three bare globals, declared in `src/ts/typings/vite-env.d.ts`
43+
and supplied by `vite.config.ts` for our own builds:
44+
45+
| Constant | Emitted literal | Meaning |
46+
| --- | --- | --- |
47+
| `__BROWSER__` | string — `"chrome"` / `"firefox"` | target browser |
48+
| `__VERSION__` | string — `"3.3.0"` | version written into the manifest |
49+
| `__MV__` | **number** — `2` / `3` | target manifest version |
50+
51+
`__MV__` is compared with strict equality (`__MV__ === 2`), so it must emit a
52+
**number literal**. Defining it as the string `"2"` makes every check silently
53+
false and the MV2 build takes MV3 code paths — no error, just wrong behavior.
54+
55+
Both bundlers do textual substitution, so the value is source text, not a JS
56+
value. `JSON.stringify()` is the safe spelling in both:
57+
58+
```js
59+
// vite
60+
define: { __BROWSER__: JSON.stringify(browser), __MV__: JSON.stringify(2) }
61+
62+
// webpack — values are code fragments, so a bare string is an identifier:
63+
// __BROWSER__: 'firefox' -> emits `firefox` -> ReferenceError
64+
new webpack.DefinePlugin({ __BROWSER__: JSON.stringify('firefox'), __MV__: 2 })
65+
```
66+
67+
**Anything that compiles this source with its own bundler must define all three**,
68+
or the bundle ships a reference to an undefined global and throws at runtime.
69+
LiveTL is the one such consumer: it builds `chat-background.ts`,
70+
`chat-injector.ts`, `chat-interceptor.ts`, `hyperchat.ts` and `options.ts` as its
71+
own entry points, so it needs these in **both** its webpack (MV2) and vite (MV3)
72+
configs. `__MV__` must match that consumer's own manifest version, not ours.
73+
74+
Used by `WelcomeMessage.svelte`, `Hyperchat.svelte`, `HyperchatButton.svelte`,
75+
`chat-background.ts`, `chat-injector.ts`. When adding a new constant, update this
76+
table — a missing define fails at runtime, not at build time.
3377

3478
## Code Patterns
3579

3680
- Prefer editing existing modules and utilities over creating one-off files for tiny helpers.
3781
- If a helper obviously belongs in an existing shared utility file, put it there.
38-
- Put shared behavior on `mv2` first; `main` should usually be a merge-plus-adaptation branch, not a separate feature branch.
39-
- Keep MV3 adaptation narrow:
40-
- preserve branch-specific build/runtime wiring
82+
- Keep MV2 adaptation narrow:
4183
- change only what is required for manifest/background/injection differences
84+
- reach for `__MV__` only when an API genuinely differs between manifest versions
4285
- Prefer render-edge formatting over mutating raw identity data:
4386
- keep parsed message/channel ids untouched
4487
- transform display text at component or view-model boundaries
@@ -61,8 +104,8 @@
61104
- name: `chrome-devtools`
62105
- command: `npx -y chrome-devtools-mcp@latest --browserUrl=http://127.0.0.1:9222`
63106
- Use `scripts/codex-dev.sh watch` once per session to keep Chrome extension builds live in the background.
64-
- On `main`, this resolves to MV3 scripts (`dev:chrome`/`build:chrome`) and `build/chrome` output automatically.
65-
- On `mv2`, watcher mode prefers `npm run start:none` so build watch stays alive without separate browser autolaunch.
107+
- The watcher resolves to MV3 Chrome scripts (`dev:chrome`/`build:chrome`) and `build/chrome` output automatically.
108+
- The harness is Chromium-only. The MV2 target is Firefox-only, so `go-test` does not cover it — validate MV2 by loading `build/mv2` in Firefox by hand.
66109
- Start headless browser testing only when explicitly requested (for example: "go test", "test this", "run browser test").
67110
- For test runs, use `scripts/codex-dev.sh go-test`. This guarantees:
68111
- MCP configuration is present

‎README.md‎

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,26 @@ See https://livetl.app/hyperchat/install
1717

1818
## Building from Source
1919

20-
### ⚠️ WARNING ⚠️
20+
### Build targets
2121

22-
For legacy reasons, we have a `mv2` branch used by [the LiveTL extension](https://github.com/LiveTL/LiveTL)'s Manifest V2 Firefox variant, while the `main` branch houses the main Manifest V3 version that's published to stores.
22+
Everything ships from `main`. There is one source tree and three build targets:
23+
24+
> Transition status: migrating both LiveTL lines to this source and deleting the
25+
> remote `mv2` rollback branch are still pending runtime validation.
26+
27+
| Target | Manifest | Consumer |
28+
| --------- | ----------------- | -------------------------------------------------------------------------------------- |
29+
| `chrome` | MV3 | Chrome Web Store |
30+
| `firefox` | MV3 | Firefox Add-ons |
31+
| `mv2` | MV2, Firefox-only | [The LiveTL extension](https://github.com/LiveTL/LiveTL)'s Manifest V2 Firefox variant |
32+
33+
> ⚠️ The `mv2` target is **not** legacy cruft. Firefox's MV3 support is unreliable
34+
> for LiveTL's needs, so LiveTL's Firefox variant consumes the MV2 build.
35+
36+
MV2/MV3 differences live in two places only: `{{mv2}}.` / `{{mv3}}.` prefixed keys
37+
in `src/manifest.json` (resolved by `scripts/resolve-manifest.ts`), and `__MV__`
38+
checks in source, which are build-time constants so each bundle carries only its
39+
own target's code.
2340

2441
### Development
2542

@@ -41,8 +58,9 @@ npm install # install dependencies
4158
Serve the extension for local development:
4259

4360
```bash
44-
npm run dev:chrome # devserver for Chrome extension
45-
npm run dev:firefox # devserver for Firefox extension
61+
npm run dev:chrome # devserver for Chrome extension (MV3)
62+
npm run dev:firefox # devserver for Firefox extension (MV3)
63+
npm run dev:mv2 # devserver for the Firefox MV2 variant
4664

4765
npm run start:chrome # devserver + open extension in Chrome
4866
npm run start:firefox # devserver + open extension in Firefox
@@ -55,13 +73,16 @@ Our build script is [an automated GitHub action](.github/workflows/release.yml),
5573
To simulate the build:
5674

5775
```bash
58-
VERSION=X.Y.Z npm run build # Chrome & Firefox
59-
VERSION=X.Y.Z npm run build:chrome # just Chrome
60-
VERSION=X.Y.Z npm run build:firefox # just Firefox
76+
VERSION=X.Y.Z npm run build # all three targets
77+
VERSION=X.Y.Z npm run build:chrome # just Chrome (MV3)
78+
VERSION=X.Y.Z npm run build:firefox # just Firefox (MV3)
79+
VERSION=X.Y.Z npm run build:mv2 # just Firefox MV2
6180
```
6281

63-
The built ZIP files can be found in the `build` directory.
82+
The built ZIP files can be found in the `build` directory. Only the two MV3 zips
83+
are published as release assets; the MV2 build is verified in CI and consumed by
84+
LiveTL through a git submodule.
6485

6586
## Release
6687

67-
Release steps for `mv2` and `main` are documented in [RELEASE_PROCESS.md](./RELEASE_PROCESS.md).
88+
Release steps are documented in [RELEASE_PROCESS.md](./RELEASE_PROCESS.md).

‎RELEASE_PROCESS.md‎

Lines changed: 50 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,80 @@
11
# HyperChat Release Process
22

3-
This repo ships from two active lines that serve different purposes.
3+
Everything ships from `main`. There is no branch ladder — MV2 and MV3 are build
4+
targets, not branches.
45

5-
## Branch Order
6+
`mv3` and `mv3-ltl` are historical. The remote `mv2` branch is rollback-only
7+
until LiveTL migration and runtime validation finish; do not route maintenance
8+
through it or delete it early.
69

7-
1. Make and validate changes on `mv2` first.
8-
2. Commit and push on `mv2`.
9-
3. Merge `mv2` into `main`.
10-
4. Validate and push `main`.
11-
5. Create releases from each branch that actually ships.
10+
## Branch Order
1211

13-
`main` is the MV3 line LiveTL consumes. `mv3` and `mv3-ltl` are historical. Do not route normal maintenance through them unless the user explicitly asks for branch archaeology.
12+
1. Make and validate changes on `main`.
13+
2. Push `main`.
14+
3. Create and publish the release tag.
1415

15-
If the same task also affects LiveTL, do not begin in LiveTL. Finish this HyperChat ladder first, then bump the LiveTL submodule chain in this order:
16+
If the same task also affects LiveTL, do not begin in LiveTL. Finish HyperChat
17+
first, then bump the LiveTL submodule chain in this order:
1618

1719
1. LiveTL `develop`
1820
2. LiveTL `mv3-fr`
1921
3. LiveTL `release`
2022

21-
HyperChat is the upstream source for shared chat behavior. LiveTL is downstream packaging/integration work after that.
23+
HyperChat is the upstream source for shared chat behavior. LiveTL is downstream
24+
packaging/integration work after that.
2225

2326
## Local Validation
2427

25-
On `mv2`:
26-
2728
```bash
28-
git checkout mv2
29-
yarn
30-
yarn build
31-
yarn package
29+
npm ci
30+
VERSION=0.0.0 npm run build # chrome + firefox (MV3) + mv2, in parallel
3231
```
3332

34-
On `main`:
35-
36-
```bash
37-
git checkout main
38-
yarn
39-
yarn build:chrome
40-
yarn build:firefox
41-
```
33+
Individual targets are `build:chrome`, `build:firefox`, and `build:mv2`. See
34+
[README.md](./README.md) for what each one is for.
4235

43-
Reinstall after switching branches. Dependency trees and lockfiles differ enough that stale installs can break builds.
36+
Reinstall after switching branches — stale installs can break builds, and the
37+
artifact comparison below is only valid against the `node_modules` that produced
38+
the baseline.
4439

45-
## Release Behavior By Branch
46-
47-
### `mv2`
40+
## Release Behavior
4841

4942
- Workflow: `.github/workflows/release.yml`
50-
- Trigger: GitHub release event `released`
51-
- Build command uses inline env injection: `VERSION=<tag> yarn build`
52-
- Result: release tag version is injected into the built manifest at build time.
43+
- Trigger: GitHub release event `published`, or `workflow_dispatch` with a tag
44+
- Version comes from the **release tag**, not `package.json`: the workflow strips
45+
the leading `v` and any suffix, then passes it as `VERSION` so Vite writes it
46+
into each manifest at build time.
5347

54-
Practical rule for `mv2`: create a release tag like `v69.43.0` and the workflow builds that version.
48+
Practical rule: create a release tag like `v3.3.0` and publish it. The workflow
49+
builds that version.
5550

56-
### `main`
51+
Published assets:
5752

58-
- Workflow: `.github/workflows/release.yml`
59-
- Trigger: GitHub release event `published`
60-
- Build version comes from `package.json`, and Vite writes it into the manifest at build time.
53+
- `HyperChat-Chrome.zip` — MV3, Chrome Web Store
54+
- `HyperChat-Firefox.zip` — MV3, Firefox Add-ons
55+
56+
The MV2 target is **not** published as a release asset. It is built on every push
57+
by `.github/workflows/build.yml` (via `npm run build`) so breakage is caught, and
58+
LiveTL consumes it through a git submodule rather than a download.
6159

62-
Practical rule for `main`: bump `package.json`, commit it, then create/publish the release tag.
60+
## Verifying A Build
6361

64-
## Recommended "Least Surprise" Publish Flow
62+
There is no test suite; build output is the oracle. To check that a change did
63+
not move the shipped artifacts, capture a baseline before the change and compare
64+
byte-for-byte after — file lists and sizes miss same-size changes:
6565

66-
1. Finish feature/fix on `mv2`, run local build/package checks, push.
67-
2. Merge to `main`, run local build checks, push.
68-
3. Create `mv2` release tag and publish release.
69-
4. Bump version state on `main`, then create/publish the `main` release tag.
70-
5. Confirm both artifacts exist on each GitHub release:
71-
- `HyperChat-Chrome.zip`
72-
- `HyperChat-Firefox.zip`
66+
```bash
67+
python3 -c "
68+
import zipfile
69+
for b in ['HyperChat-chrome.zip','HyperChat-firefox.zip']:
70+
a=zipfile.ZipFile('/tmp/golden/'+b); c=zipfile.ZipFile('build/'+b)
71+
d=[n for n in sorted(set(a.namelist())) if a.read(n)!=c.read(n)]
72+
print(b, d or 'all bytes match')
73+
"
74+
```
7375

7476
## Notes For LiveTL Sync
7577

76-
LiveTL consumes HyperChat via submodules. Keep HyperChat changes branch-aligned across `mv2` and `main` so LiveTL can take them with straightforward submodule bumps in its release flow. LiveTL's MV2 Firefox variant tracks `mv2`; its MV3 line tracks `main`.
78+
LiveTL consumes HyperChat via git submodules, not release assets. Its MV2 Firefox
79+
variant and MV3 line will both track `main` after the downstream migration. Until
80+
their release pins and runtime checks pass, keep the remote `mv2` rollback branch.

‎package.json‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
"private": true,
55
"scripts": {
66
"typecheck": "tsc --noEmit",
7-
"build": "npm run typecheck && npm-run-all --parallel build:chrome build:firefox",
7+
"build": "npm run typecheck && npm-run-all --parallel build:chrome build:firefox build:mv2 && npm run verify:build",
88
"build:chrome": "cross-env BROWSER=chrome vite build",
99
"build:firefox": "cross-env BROWSER=firefox vite build",
10+
"build:mv2": "cross-env BROWSER=firefox MV=2 vite build",
1011
"dev:chrome": "cross-env MINIFY=false BROWSER=chrome vite build --watch",
1112
"dev:firefox": "cross-env MINIFY=false BROWSER=firefox vite build --watch",
13+
"dev:mv2": "cross-env MINIFY=false BROWSER=firefox MV=2 vite build --watch",
14+
"verify:build": "node scripts/verify-build.mjs",
1215
"start": "npm run dev:chrome",
1316
"start:none": "npm run dev:chrome",
1417
"start:chrome": "cross-env AUTOLAUNCH=true npm run dev:chrome",

‎scripts/resolve-manifest.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Resolves `{{mv2}}.` / `{{mv3}}.` key prefixes in the manifest against the
3+
* target manifest version, dropping keys tagged for the other one.
4+
*
5+
* Browser tags (`{{chrome}}.` / `{{firefox}}.`) are deliberately left in place:
6+
* vite-plugin-web-extension resolves those itself from its `browser` option.
7+
*
8+
* Both resolvers only match a *leading* prefix, so the MV tag has to come first
9+
* — `{{mv3}}.{{firefox}}.background`, never `{{firefox}}.{{mv3}}.background`.
10+
* The reverse order leaves a literal `{{mv3}}.` key in the emitted manifest.
11+
*/
12+
13+
const mvTag = /^\{\{mv([23])\}\}\./;
14+
15+
export const resolveMv = (value: any, mv: 2 | 3): any => {
16+
if (Array.isArray(value)) return value.map(item => resolveMv(item, mv));
17+
if (typeof value !== 'object' || value === null) return value;
18+
19+
const resolved: Record<string, any> = {};
20+
for (const [key, child] of Object.entries(value)) {
21+
const tag = key.match(mvTag);
22+
if (tag && parseInt(tag[1], 10) !== mv) continue;
23+
resolved[tag ? key.slice(tag[0].length) : key] = resolveMv(child, mv);
24+
}
25+
return resolved;
26+
};

0 commit comments

Comments
 (0)