diff --git a/AGENTS.md b/AGENTS.md index 0d71df1c..29717f5c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,19 +1,16 @@ -# HyperChat Codex Workflow (MV3 Main) +# HyperChat Codex Workflow ## Branch Discipline -- Make code changes on `mv2` first. -- `main` is the real MV3 branch. -- `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. -- Do not implement feature/fix work directly on `main`. -- If a task starts on another branch, switch to `mv2` before editing unless the user explicitly asks otherwise. +- `main` is the only maintained source branch. Make code changes there. +- `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. +- There is no branch ladder any more. MV2 and MV3 are **build targets on `main`**, not branches — see "Manifest Version Targets" below. - If a task touches both HyperChat and LiveTL, HyperChat still goes first. - Cross-repo order is mandatory: - 1. HyperChat `mv2` - 2. HyperChat `main` - 3. LiveTL `develop` - 4. LiveTL `mv3-fr` - 5. LiveTL `release` + 1. HyperChat `main` + 2. LiveTL `develop` + 3. LiveTL `mv3-fr` + 4. LiveTL `release` - Never start cross-repo work in LiveTL when the HyperChat submodule also needs to change. - If the task also requires syncing YtcFilter (YTCF), do it after HyperChat `main` is updated: - merge HyperChat `main` into YTCF `master` @@ -28,17 +25,63 @@ - `order matters` - Avoid padded scopes, issue-number prefixes, and changelog-style essays in commit subjects. - A slightly dry or funny commit is fine if it is still clear at a glance. -- Prefer proper merges when carrying `mv2` work into `main`. -- If MV3 needs follow-up adaptation, keep that as a small, explicit commit after the merge instead of rewriting history or hand-copying changes. + +## Manifest Version Targets + +- `main` builds three targets: `chrome` (MV3), `firefox` (MV3), and `mv2` (MV2, Firefox-only). +- 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. +- Keep the MV2/MV3 distinction in exactly two places: + - `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. + - `__MV__` in source — a build-time constant, so unused branches are dropped per target. +- 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. +- Prefer shared, untagged config. Only tag a key when MV2 and MV3 genuinely differ. +- Only the two MV3 zips are released. `mv2` is built in CI for breakage coverage and consumed by LiveTL via submodule. + +## Build-Time Constants (submodule consumers) + +`src/` depends on three bare globals, declared in `src/ts/typings/vite-env.d.ts` +and supplied by `vite.config.ts` for our own builds: + +| Constant | Emitted literal | Meaning | +| --- | --- | --- | +| `__BROWSER__` | string — `"chrome"` / `"firefox"` | target browser | +| `__VERSION__` | string — `"3.3.0"` | version written into the manifest | +| `__MV__` | **number** — `2` / `3` | target manifest version | + +`__MV__` is compared with strict equality (`__MV__ === 2`), so it must emit a +**number literal**. Defining it as the string `"2"` makes every check silently +false and the MV2 build takes MV3 code paths — no error, just wrong behavior. + +Both bundlers do textual substitution, so the value is source text, not a JS +value. `JSON.stringify()` is the safe spelling in both: + +```js +// vite +define: { __BROWSER__: JSON.stringify(browser), __MV__: JSON.stringify(2) } + +// webpack — values are code fragments, so a bare string is an identifier: +// __BROWSER__: 'firefox' -> emits `firefox` -> ReferenceError +new webpack.DefinePlugin({ __BROWSER__: JSON.stringify('firefox'), __MV__: 2 }) +``` + +**Anything that compiles this source with its own bundler must define all three**, +or the bundle ships a reference to an undefined global and throws at runtime. +LiveTL is the one such consumer: it builds `chat-background.ts`, +`chat-injector.ts`, `chat-interceptor.ts`, `hyperchat.ts` and `options.ts` as its +own entry points, so it needs these in **both** its webpack (MV2) and vite (MV3) +configs. `__MV__` must match that consumer's own manifest version, not ours. + +Used by `WelcomeMessage.svelte`, `Hyperchat.svelte`, `HyperchatButton.svelte`, +`chat-background.ts`, `chat-injector.ts`. When adding a new constant, update this +table — a missing define fails at runtime, not at build time. ## Code Patterns - Prefer editing existing modules and utilities over creating one-off files for tiny helpers. - If a helper obviously belongs in an existing shared utility file, put it there. -- Put shared behavior on `mv2` first; `main` should usually be a merge-plus-adaptation branch, not a separate feature branch. -- Keep MV3 adaptation narrow: - - preserve branch-specific build/runtime wiring +- Keep MV2 adaptation narrow: - change only what is required for manifest/background/injection differences + - reach for `__MV__` only when an API genuinely differs between manifest versions - Prefer render-edge formatting over mutating raw identity data: - keep parsed message/channel ids untouched - transform display text at component or view-model boundaries @@ -61,8 +104,8 @@ - name: `chrome-devtools` - command: `npx -y chrome-devtools-mcp@latest --browserUrl=http://127.0.0.1:9222` - Use `scripts/codex-dev.sh watch` once per session to keep Chrome extension builds live in the background. -- On `main`, this resolves to MV3 scripts (`dev:chrome`/`build:chrome`) and `build/chrome` output automatically. -- On `mv2`, watcher mode prefers `npm run start:none` so build watch stays alive without separate browser autolaunch. +- The watcher resolves to MV3 Chrome scripts (`dev:chrome`/`build:chrome`) and `build/chrome` output automatically. +- 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. - Start headless browser testing only when explicitly requested (for example: "go test", "test this", "run browser test"). - For test runs, use `scripts/codex-dev.sh go-test`. This guarantees: - MCP configuration is present diff --git a/README.md b/README.md index e29fe870..cb9e93b3 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,26 @@ See https://livetl.app/hyperchat/install ## Building from Source -### ⚠️ WARNING ⚠️ +### Build targets -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. +Everything ships from `main`. There is one source tree and three build targets: + +> Transition status: migrating both LiveTL lines to this source and deleting the +> remote `mv2` rollback branch are still pending runtime validation. + +| Target | Manifest | Consumer | +| --------- | ----------------- | -------------------------------------------------------------------------------------- | +| `chrome` | MV3 | Chrome Web Store | +| `firefox` | MV3 | Firefox Add-ons | +| `mv2` | MV2, Firefox-only | [The LiveTL extension](https://github.com/LiveTL/LiveTL)'s Manifest V2 Firefox variant | + +> ⚠️ The `mv2` target is **not** legacy cruft. Firefox's MV3 support is unreliable +> for LiveTL's needs, so LiveTL's Firefox variant consumes the MV2 build. + +MV2/MV3 differences live in two places only: `{{mv2}}.` / `{{mv3}}.` prefixed keys +in `src/manifest.json` (resolved by `scripts/resolve-manifest.ts`), and `__MV__` +checks in source, which are build-time constants so each bundle carries only its +own target's code. ### Development @@ -41,8 +58,9 @@ npm install # install dependencies Serve the extension for local development: ```bash -npm run dev:chrome # devserver for Chrome extension -npm run dev:firefox # devserver for Firefox extension +npm run dev:chrome # devserver for Chrome extension (MV3) +npm run dev:firefox # devserver for Firefox extension (MV3) +npm run dev:mv2 # devserver for the Firefox MV2 variant npm run start:chrome # devserver + open extension in Chrome 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), To simulate the build: ```bash -VERSION=X.Y.Z npm run build # Chrome & Firefox -VERSION=X.Y.Z npm run build:chrome # just Chrome -VERSION=X.Y.Z npm run build:firefox # just Firefox +VERSION=X.Y.Z npm run build # all three targets +VERSION=X.Y.Z npm run build:chrome # just Chrome (MV3) +VERSION=X.Y.Z npm run build:firefox # just Firefox (MV3) +VERSION=X.Y.Z npm run build:mv2 # just Firefox MV2 ``` -The built ZIP files can be found in the `build` directory. +The built ZIP files can be found in the `build` directory. Only the two MV3 zips +are published as release assets; the MV2 build is verified in CI and consumed by +LiveTL through a git submodule. ## Release -Release steps for `mv2` and `main` are documented in [RELEASE_PROCESS.md](./RELEASE_PROCESS.md). +Release steps are documented in [RELEASE_PROCESS.md](./RELEASE_PROCESS.md). diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md index dd24c6a5..677deeb7 100644 --- a/RELEASE_PROCESS.md +++ b/RELEASE_PROCESS.md @@ -1,76 +1,80 @@ # HyperChat Release Process -This repo ships from two active lines that serve different purposes. +Everything ships from `main`. There is no branch ladder — MV2 and MV3 are build +targets, not branches. -## Branch Order +`mv3` and `mv3-ltl` are historical. The remote `mv2` branch is rollback-only +until LiveTL migration and runtime validation finish; do not route maintenance +through it or delete it early. -1. Make and validate changes on `mv2` first. -2. Commit and push on `mv2`. -3. Merge `mv2` into `main`. -4. Validate and push `main`. -5. Create releases from each branch that actually ships. +## Branch Order -`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. +1. Make and validate changes on `main`. +2. Push `main`. +3. Create and publish the release tag. -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: +If the same task also affects LiveTL, do not begin in LiveTL. Finish HyperChat +first, then bump the LiveTL submodule chain in this order: 1. LiveTL `develop` 2. LiveTL `mv3-fr` 3. LiveTL `release` -HyperChat is the upstream source for shared chat behavior. LiveTL is downstream packaging/integration work after that. +HyperChat is the upstream source for shared chat behavior. LiveTL is downstream +packaging/integration work after that. ## Local Validation -On `mv2`: - ```bash -git checkout mv2 -yarn -yarn build -yarn package +npm ci +VERSION=0.0.0 npm run build # chrome + firefox (MV3) + mv2, in parallel ``` -On `main`: - -```bash -git checkout main -yarn -yarn build:chrome -yarn build:firefox -``` +Individual targets are `build:chrome`, `build:firefox`, and `build:mv2`. See +[README.md](./README.md) for what each one is for. -Reinstall after switching branches. Dependency trees and lockfiles differ enough that stale installs can break builds. +Reinstall after switching branches — stale installs can break builds, and the +artifact comparison below is only valid against the `node_modules` that produced +the baseline. -## Release Behavior By Branch - -### `mv2` +## Release Behavior - Workflow: `.github/workflows/release.yml` -- Trigger: GitHub release event `released` -- Build command uses inline env injection: `VERSION= yarn build` -- Result: release tag version is injected into the built manifest at build time. +- Trigger: GitHub release event `published`, or `workflow_dispatch` with a tag +- Version comes from the **release tag**, not `package.json`: the workflow strips + the leading `v` and any suffix, then passes it as `VERSION` so Vite writes it + into each manifest at build time. -Practical rule for `mv2`: create a release tag like `v69.43.0` and the workflow builds that version. +Practical rule: create a release tag like `v3.3.0` and publish it. The workflow +builds that version. -### `main` +Published assets: -- Workflow: `.github/workflows/release.yml` -- Trigger: GitHub release event `published` -- Build version comes from `package.json`, and Vite writes it into the manifest at build time. +- `HyperChat-Chrome.zip` — MV3, Chrome Web Store +- `HyperChat-Firefox.zip` — MV3, Firefox Add-ons + +The MV2 target is **not** published as a release asset. It is built on every push +by `.github/workflows/build.yml` (via `npm run build`) so breakage is caught, and +LiveTL consumes it through a git submodule rather than a download. -Practical rule for `main`: bump `package.json`, commit it, then create/publish the release tag. +## Verifying A Build -## Recommended "Least Surprise" Publish Flow +There is no test suite; build output is the oracle. To check that a change did +not move the shipped artifacts, capture a baseline before the change and compare +byte-for-byte after — file lists and sizes miss same-size changes: -1. Finish feature/fix on `mv2`, run local build/package checks, push. -2. Merge to `main`, run local build checks, push. -3. Create `mv2` release tag and publish release. -4. Bump version state on `main`, then create/publish the `main` release tag. -5. Confirm both artifacts exist on each GitHub release: - - `HyperChat-Chrome.zip` - - `HyperChat-Firefox.zip` +```bash +python3 -c " +import zipfile +for b in ['HyperChat-chrome.zip','HyperChat-firefox.zip']: + a=zipfile.ZipFile('/tmp/golden/'+b); c=zipfile.ZipFile('build/'+b) + d=[n for n in sorted(set(a.namelist())) if a.read(n)!=c.read(n)] + print(b, d or 'all bytes match') +" +``` ## Notes For LiveTL Sync -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`. +LiveTL consumes HyperChat via git submodules, not release assets. Its MV2 Firefox +variant and MV3 line will both track `main` after the downstream migration. Until +their release pins and runtime checks pass, keep the remote `mv2` rollback branch. diff --git a/package.json b/package.json index b3abb41a..c30ef63e 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,14 @@ "private": true, "scripts": { "typecheck": "tsc --noEmit", - "build": "npm run typecheck && npm-run-all --parallel build:chrome build:firefox", + "build": "npm run typecheck && npm-run-all --parallel build:chrome build:firefox build:mv2 && npm run verify:build", "build:chrome": "cross-env BROWSER=chrome vite build", "build:firefox": "cross-env BROWSER=firefox vite build", + "build:mv2": "cross-env BROWSER=firefox MV=2 vite build", "dev:chrome": "cross-env MINIFY=false BROWSER=chrome vite build --watch", "dev:firefox": "cross-env MINIFY=false BROWSER=firefox vite build --watch", + "dev:mv2": "cross-env MINIFY=false BROWSER=firefox MV=2 vite build --watch", + "verify:build": "node scripts/verify-build.mjs", "start": "npm run dev:chrome", "start:none": "npm run dev:chrome", "start:chrome": "cross-env AUTOLAUNCH=true npm run dev:chrome", diff --git a/scripts/resolve-manifest.ts b/scripts/resolve-manifest.ts new file mode 100644 index 00000000..711b4f43 --- /dev/null +++ b/scripts/resolve-manifest.ts @@ -0,0 +1,26 @@ +/** + * Resolves `{{mv2}}.` / `{{mv3}}.` key prefixes in the manifest against the + * target manifest version, dropping keys tagged for the other one. + * + * Browser tags (`{{chrome}}.` / `{{firefox}}.`) are deliberately left in place: + * vite-plugin-web-extension resolves those itself from its `browser` option. + * + * Both resolvers only match a *leading* prefix, so the MV tag has to come first + * — `{{mv3}}.{{firefox}}.background`, never `{{firefox}}.{{mv3}}.background`. + * The reverse order leaves a literal `{{mv3}}.` key in the emitted manifest. + */ + +const mvTag = /^\{\{mv([23])\}\}\./; + +export const resolveMv = (value: any, mv: 2 | 3): any => { + if (Array.isArray(value)) return value.map(item => resolveMv(item, mv)); + if (typeof value !== 'object' || value === null) return value; + + const resolved: Record = {}; + for (const [key, child] of Object.entries(value)) { + const tag = key.match(mvTag); + if (tag && parseInt(tag[1], 10) !== mv) continue; + resolved[tag ? key.slice(tag[0].length) : key] = resolveMv(child, mv); + } + return resolved; +}; diff --git a/scripts/verify-build.mjs b/scripts/verify-build.mjs new file mode 100644 index 00000000..80975638 --- /dev/null +++ b/scripts/verify-build.mjs @@ -0,0 +1,82 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access */ +import assert from 'node:assert/strict'; +import { access, readFile } from 'node:fs/promises'; +import path from 'node:path'; + +const hostPermissions = [ + 'https://www.youtube.com/live_chat*', + 'https://www.youtube.com/live_chat_replay*', + 'https://studio.youtube.com/live_chat*', + 'https://studio.youtube.com/live_chat_replay*' +]; +const firefoxSettings = { + gecko: { + id: '{14a15c41-13f4-498e-986c-7f00435c4d00}', + strict_min_version: '115.0' + } +}; + +const unresolvedKeys = (value, result = []) => { + if (Array.isArray(value)) { + value.forEach(child => unresolvedKeys(child, result)); + } else if (value !== null && typeof value === 'object') { + for (const [key, child] of Object.entries(value)) { + if (/\{\{[^}]+\}\}\./.test(key)) result.push(key); + unresolvedKeys(child, result); + } + } + return result; +}; + +for (const target of ['chrome', 'firefox', 'mv2']) { + const buildDir = path.resolve('build', target); + const manifest = JSON.parse(await readFile(path.join(buildDir, 'manifest.json'), 'utf8')); + const mv = target === 'mv2' ? 2 : 3; + + assert.equal(manifest.manifest_version, mv, `${target}: wrong manifest version`); + assert.deepEqual(unresolvedKeys(manifest), [], `${target}: unresolved manifest key`); + + if (target === 'chrome') { + assert.equal(typeof manifest.background?.service_worker, 'string', 'chrome: missing service worker'); + assert.equal(manifest.background?.scripts, undefined, 'chrome: unexpected background scripts'); + } else { + assert.ok(Array.isArray(manifest.background?.scripts), `${target}: missing background scripts`); + assert.equal(manifest.background?.service_worker, undefined, `${target}: unexpected service worker`); + } + + if (mv === 2) { + assert.equal(manifest.background.persistent, true, 'mv2: background must be persistent'); + assert.ok(manifest.browser_action, 'mv2: missing browser_action'); + assert.equal(manifest.action, undefined, 'mv2: unexpected action'); + assert.equal(manifest.host_permissions, undefined, 'mv2: unexpected host permissions'); + assert.equal(manifest.browser_specific_settings, undefined, 'mv2: unexpected Firefox metadata'); + assert.deepEqual(manifest.web_accessible_resources, ['*'], 'mv2: wrong web-accessible resources'); + } else { + assert.equal(manifest.background.persistent, undefined, `${target}: persistent background`); + assert.ok(manifest.action, `${target}: missing action`); + assert.equal(manifest.browser_action, undefined, `${target}: unexpected browser_action`); + assert.deepEqual(manifest.host_permissions, hostPermissions, `${target}: wrong host permissions`); + assert.ok(manifest.web_accessible_resources?.[0]?.resources, `${target}: wrong web-accessible resources`); + if (target === 'firefox') { + assert.deepEqual(manifest.browser_specific_settings, firefoxSettings, 'firefox: wrong metadata'); + } else { + assert.equal(manifest.browser_specific_settings, undefined, 'chrome: unexpected Firefox metadata'); + } + } + + const files = [ + ...manifest.content_scripts.flatMap(script => [...(script.js ?? []), ...(script.css ?? [])]), + ...(manifest.background.scripts ?? []), + manifest.background.service_worker, + manifest.options_page, + manifest.options_ui?.page, + manifest.action?.default_popup, + manifest.browser_action?.default_popup + ].filter(file => typeof file === 'string'); + + for (const file of files) { + await assert.doesNotReject(access(path.join(buildDir, file)), `${target}: missing ${file}`); + } +} + +console.log('Verified chrome, firefox, and mv2 builds.'); diff --git a/src/manifest.json b/src/manifest.json index bc7b7c91..ae61d7c5 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,5 +1,6 @@ { - "manifest_version": 3, + "{{mv3}}.manifest_version": 3, + "{{mv2}}.manifest_version": 2, "name": "HyperChat [Improved YouTube Chat]", "version": "0.0.0", "homepage_url": "https://livetl.app/hyperchat", @@ -11,7 +12,7 @@ "permissions": [ "storage" ], - "host_permissions": [ + "{{mv3}}.host_permissions": [ "https://www.youtube.com/live_chat*", "https://www.youtube.com/live_chat_replay*", "https://studio.youtube.com/live_chat*", @@ -47,31 +48,42 @@ "all_frames": true } ], - "{{firefox}}.background": { + "{{mv3}}.{{firefox}}.background": { "scripts": ["scripts/chat-background.ts"] }, - "{{chrome}}.background": { + "{{mv3}}.{{chrome}}.background": { "service_worker": "scripts/chat-background.ts" }, - "action": { + "{{mv2}}.background": { + "scripts": ["scripts/chat-background.ts"], + "persistent": true + }, + "{{mv3}}.action": { "default_icon": { "48": "assets/logo-48.png", "128": "assets/logo-128.png" }, "default_popup": "options.html" }, + "{{mv2}}.browser_action": { + "default_icon": { + "48": "assets/logo-48.png", + "128": "assets/logo-128.png" + } + }, "options_ui": { "page": "options.html", "open_in_tab": true }, - "web_accessible_resources": [ + "{{mv3}}.web_accessible_resources": [ { "resources": ["*"], "matches": [""] } ], + "{{mv2}}.web_accessible_resources": ["*"], "{{chrome}}.incognito": "split", - "{{firefox}}.browser_specific_settings": { + "{{mv3}}.{{firefox}}.browser_specific_settings": { "gecko": { "id": "{14a15c41-13f4-498e-986c-7f00435c4d00}", "strict_min_version": "115.0" diff --git a/src/scripts/chat-background.ts b/src/scripts/chat-background.ts index 661feb90..0d0350c0 100644 --- a/src/scripts/chat-background.ts +++ b/src/scripts/chat-background.ts @@ -6,7 +6,10 @@ const oneDay = 1000 * 60 * 60 * 24; const storageget = (key: string): any => chrome.storage.local.get(key).then(r => r[key]); const defaultTo0 = (value: any): number => Number.isNaN(value) ? 0 : value; -chrome.action.onClicked.addListener(() => { +// MV2 has no `chrome.action`; `chrome.browserAction` is its equivalent. +const browserAction = __MV__ === 2 ? chrome.browserAction : chrome.action; + +browserAction.onClicked.addListener(() => { if (isLiveTL) { chrome.tabs.create({ url: 'https://livetl.app' }, () => {}); } else { diff --git a/src/scripts/chat-injector.ts b/src/scripts/chat-injector.ts index c7a9f6cf..dcd8c597 100644 --- a/src/scripts/chat-injector.ts +++ b/src/scripts/chat-injector.ts @@ -164,8 +164,13 @@ const chatLoaded = async (): Promise => { params.set('tabid', frameInfo.tabId.toString()); params.set('frameid', frameInfo.frameId.toString()); if (frameIsReplay()) params.set('isReplay', 'true'); - // inject into an empty 404 page - const source = `https://www.youtube.com/embed/hyperchat_embed?${params.toString()}`; + // MV2 can iframe the extension page directly. MV3 cannot, so it points at an + // empty YouTube 404 page and lets chat-mounter mount into it instead. + const source = __MV__ === 2 + ? chrome.runtime.getURL( + (isLiveTL ? 'hyperchat/index.html' : 'hyperchat.html') + `?${params.toString()}` + ) + : `https://www.youtube.com/embed/hyperchat_embed?${params.toString()}`; const ytcItemList = document.querySelector('#chat>#item-list'); if (!ytcItemList) { diff --git a/src/ts/typings/vite-env.d.ts b/src/ts/typings/vite-env.d.ts index f850b2d0..584b86be 100644 --- a/src/ts/typings/vite-env.d.ts +++ b/src/ts/typings/vite-env.d.ts @@ -1,6 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ declare const __BROWSER__: 'chrome' | 'firefox'; declare const __VERSION__: string; +/** Target manifest version. Build-time constant, so `__MV__` branches are + * dead-code-eliminated and each bundle only carries its own MV's code. */ +declare const __MV__: 2 | 3; // Vite CSS imports with ?inline suffix declare module '*?inline' { diff --git a/tsconfig.json b/tsconfig.json index fa5b1204..88279c67 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "src/**/*", "*.js", "utils/**/*", + "scripts/**/*", "vite.config.ts" ], "exclude": [ diff --git a/vite.config.ts b/vite.config.ts index 7399eab7..e63f42cb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,14 +4,18 @@ import copy from 'rollup-plugin-copy'; import { defineConfig } from 'vite'; import webExtension, { readJsonFile } from 'vite-plugin-web-extension'; import zipPack from 'vite-plugin-zip-pack'; +import { resolveMv } from './scripts/resolve-manifest'; const pkg = readJsonFile('package.json'); const manifest = readJsonFile('src/manifest.json'); const browser = process.env.BROWSER ?? 'chrome'; +const mv = process.env.MV === '2' ? 2 : 3; const version = process.env.VERSION ?? pkg.version; -const buildDir = `build/${browser}`; +// MV2 is Firefox-only, so it needs no per-browser output dir of its own. +const target = mv === 2 ? 'mv2' : browser; +const buildDir = `build/${target}`; export default defineConfig({ root: 'src', @@ -22,12 +26,13 @@ export default defineConfig({ }, define: { __BROWSER__: JSON.stringify(browser), - __VERSION__: JSON.stringify(version) + __VERSION__: JSON.stringify(version), + __MV__: JSON.stringify(mv) }, plugins: [ webExtension({ manifest: () => ({ - ...manifest, + ...resolveMv(manifest, mv), version }), assets: 'assets', @@ -59,7 +64,7 @@ export default defineConfig({ zipPack({ inDir: buildDir, outDir: 'build', - outFileName: `HyperChat-${browser}.zip` + outFileName: `HyperChat-${target}.zip` }) ] });