Compare commits

..

583 Commits

Author SHA1 Message Date
caorushizi d654368290 feat(core): add cobra cli entrypoint 2026-05-17 03:37:28 +08:00
y2hlbg ab67f6b409 docs: remove commercial entrypoints
Deploy MediaGo Docs / Build Docs (push) Failing after 1s
2026-04-28 19:14:58 +08:00
y2hlbg 3eb7f13eca docs(readme): update 52pojie credit note 2026-04-28 15:03:57 +08:00
y2hlbg 3b279e4a05 docs(readme): add 52pojie credit note 2026-04-28 15:03:57 +08:00
caorushizi ed03781692 fix: package.json name 2026-04-28 00:28:27 +08:00
y2hlbg fb9dd15186 fix(docs): restore canonical site url 2026-04-26 20:52:58 +08:00
y2hlbg 115572c513 feat(docs): add seo blog pillar pages 2026-04-26 20:24:22 +08:00
y2hlbg 1aa2bcd511 docs: add community links to readme 2026-04-23 10:08:23 +08:00
Charlie 248237d593 feat(i18n): add Italian localization (#646) 2026-04-23 09:04:18 +08:00
caorushizi 99281d89f9 docs: refresh VitePress homepages and add download API reference
A batch of VitePress site updates around the v3.5.0 release:

- Refresh the zh / en / jp homepages into a 3x3 feature grid aligned
  with README v3.5.0: keep the foundational "no packet capture /
  batch download / mobile playback" cards, add v3.5.0 pillars
  (browser extension / broad video source coverage / OpenClaw Skill /
  HTTP API / built-in format conversion / one-line Docker). Rewrite
  the hero with a tighter "cross-platform video downloader + built-in
  sniffing" tagline and point the English hero at the
  English-localized `home_en.png`.
- Fix en / jp hero action links that previously routed to the Chinese
  root pages (`/guides` → `/en/guides`, `/jp/guides` etc.).
- Add the macOS usage block to the en / jp quick-start guides
  (Intel vs Apple Silicon + `sudo xattr -dr com.apple.quarantine`
  unblock command), and drop the QQ feedback group tip from those
  locales — QQ is China-only, not useful for en / jp audiences.
- Add a new Download API reference page (`/api`) for all three locales,
  registered in the sidebar under "开始 / Quick start / 始める".
  Covers Base URL (desktop `39719`, Docker `9900`), auth (API key from
  Settings page), curl quick-start, download SSE events, and the full
  `/api/downloads/*` endpoint reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 01:12:17 +08:00
caorushizi aa24ef5966 docs(readme): use English-localized screenshots in English README
The English README was reusing the Chinese-localized screenshots
(`home.png`, `home-dark.png`, `settings.png`, `browser.png`), which
left English readers staring at a UI they could not read. Ship a
parallel set of English screenshots under the `_en` suffix and point
`README.md` at them; `README.zh.md` / `README.jp.md` keep their own
assets untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:29:58 +08:00
caorushizi e94b91f2f7 docs(i18n): add TRANSLATION.md contribution guide
Addresses #638, where a prospective Italian translator asked for clearer
translator onboarding and a way to preview translations in place. The new
guide documents where strings live, the four-file registration path for a
new language, and the Vite HMR dev workflow that already solves the live
preview need. Linked from all three READMEs alongside CONTRIBUTING.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:13:22 +08:00
Charlie fc99be56df Merge pull request #640 from caorushizi/fix/installer-metadata
fix(installer): show version in title bar, set installer FileDescription (#637)
2026-04-22 23:40:23 +08:00
Charlie 34bbe42600 Merge pull request #639 from bovirus/master
Add Italian langauge
2026-04-22 23:36:50 +08:00
caorushizi dfcdf6a6a5 fix(installer): show version in title bar, set installer FileDescription (#637)
- `installer/installer.nsh`: customHeader macro sets Caption to
  "Setup - ${PRODUCT_NAME} ${VERSION}" so users can see which release
  they're installing from the window title (the default $(^SetupCaption)
  omits the version, and re-setting Name trips NSIS warning 6029 which
  electron-builder's -WX flag treats as a hard error).
- `scripts/build.ts`: afterAllArtifactBuild hook runs the app-builder
  rcedit helper on the generated NSIS installer to rewrite its
  FileDescription to "${APP_NAME} installer". electron-builder's
  NsisTarget.computeVersionKey() hardcodes VIAddVersionKey /LANG=1033
  "FileDescription" "${appInfo.description}", binding the installer's
  FileDescription to the app binary's (both drawn from package.json
  description); any in-NSIS override collides on the same LANG+key
  with a hard "already defined!" error that -WX does not gate.
  Post-processing with rcedit sidesteps this and lets installer and
  app binary carry distinct descriptions — same approach VS Code's
  Inno Setup pipeline uses (where "{AppName} Setup" is the default).

Closes #637

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:23:51 +08:00
bovirus f395d49a10 GUI - Add Italian language 2026-04-22 17:17:10 +02:00
bovirus f6a228f318 Browser extension - Add italian language 2026-04-22 16:41:46 +02:00
caorushizi ba958b9f44 docs: flatten Docker feature-section command to a single line
Collapses the line-continuation form (`docker run ... \\n -v ... \\n caorushizi/mediago:3.5.0`)
into one line across README.md / README.jp.md so all three language
READMEs match (zh was already single-line). Makes the command easier
to copy-paste — readers don't have to deal with trailing backslashes
that some terminals / browsers mangle when highlighting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:15:15 +08:00
caorushizi 19e49e89b9 docs: list Docker Hub alongside GHCR in README and docs
The CI workflow mirrors every built image to Docker Hub
(`caorushizi/mediago`), but the install lists in README (zh/en/jp)
and docs (guides / changelog for all three languages) only mentioned
GHCR. Most users default to Docker Hub, so they would miss the
simpler pull path.

Switch every Docker row into a two-line form showing both registries
explicitly:

    - [**Docker Hub**](https://hub.docker.com/r/caorushizi/mediago): docker run ... caorushizi/mediago:3.5.0
    - **GHCR**: docker run ... ghcr.io/caorushizi/mediago:3.5.0

Docker Hub appears first (shorter image name, more familiar to most
users) and its label is a link to the Hub page. GHCR is kept as a
second bullet for users who prefer it or are already authenticated.

The README feature-section (`🐳 Docker deployment`) code block was
also switched to the unqualified `caorushizi/mediago:3.5.0` form so
the showcase command defaults to Docker Hub, with a prose note that
the same image lives on GHCR too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:09:57 +08:00
caorushizi 3f889da389 fix(ci): move secrets check off if: to dodge Unrecognized named-value: 'secrets'
The Docker Hub mirror workflow used `secrets.DOCKERHUB_USERNAME != ''`
directly inside step `if:` conditions and step-level `env:`
expressions. GitHub's expression parser now rejects that with
`Unrecognized named-value: 'secrets'` — the `secrets` context is only
allowed in a narrow set of positions (mostly `with:` inputs and step
`env:` values, not inside `if:` expressions).

Route the check through a dedicated detection step whose `env:` reads
the secrets (the one permitted way to surface them) and writes a
plain boolean to step outputs. Every downstream conditional then
guards on `steps.dockerhub.outputs.enabled == 'true'` instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:46:52 +08:00
caorushizi ab669f2e74 ci(docker): mirror image to Docker Hub alongside GHCR
Every image published to `ghcr.io/caorushizi/mediago` now also gets
pushed to `docker.io/caorushizi/mediago` under the same tags, so users
can `docker pull caorushizi/mediago:3.5.0` without setting up GHCR
auth.

- New `DOCKERHUB_IMAGE` env var pinning the Docker Hub slug.
- New `Login to Docker Hub` step guarded by
  `secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != ''`
  so forks without Docker Hub credentials can still build + push to
  GHCR without failing the login.
- New `Resolve image targets` step that composes the metadata-action
  `images:` list at runtime — only adds the Docker Hub target when
  both secrets are present. Without this, metadata-action would keep
  emitting Docker Hub tags that build-push-action would then 401 on.
- Build Summary shows both `docker pull` hints when Docker Hub is
  enabled.

Setup (one-time, in repo Settings → Secrets and variables → Actions):
- `DOCKERHUB_USERNAME`: Docker Hub account name
- `DOCKERHUB_TOKEN`:    PAT from https://hub.docker.com/settings/security
                        with Read+Write scope on caorushizi/mediago

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:10:33 +08:00
caorushizi 5386bfc8a2 docs: bump guides / changelog to v3.5.0 across zh / en / jp
`docs/guides.md` still linked to v3.5.0-beta.0 assets under a stale
v3.0.1 heading; the English and Japanese versions lagged further
behind at v3.0.0. Update the "Download and install" block on each
guide to point at v3.5.0 (installer / portable / macOS arm64 + x64 /
Linux / Docker) with a link back to the GitHub Releases page for
older builds.

Prepend a v3.5.0 entry to each `changelog.md` using the same
user-facing highlights the README now uses (browser extension /
YouTube + 1000+ sites / OpenClaw Skill / HTTP API / in-app format
conversion / Docker simplification / Go backend). Older entries
(v3.0.0 and earlier) are left untouched — changelog is append-only.

`docs/history.md` intentionally left alone: it's the "old permalinks"
archive and doesn't track the current release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:10:13 +08:00
caorushizi aa998d9a3a chore: add build:docker npm script for local image builds
Wraps `docker build -t mediago:local .` so contributors don't have to
remember the tag convention, and keeps local Docker builds discoverable
alongside pack:electron / release:electron in `pnpm run`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:11:37 +08:00
caorushizi dde75321a7 fix(docker): copy extra/ into build context so aria2 ships in the image
`scripts/download-deps.ts` treats the aria2 entry as `source: "local"`
and copies from `extra/aria2/<os>/<arch>/aria2c` into `.deps/`. Every
other downloader (ffmpeg / N_m3u8DL-RE / BBDown / yt-dlp / mediago) is
`source: "github"` and fetched over the network, so they reach the
image fine — but the Docker build context never included `extra/`,
which made `copyLocalTool()` hit its "source missing" branch and
silently skip aria2. Resulting image had no `aria2c`, any direct
download inside the container failed with a binary-not-found error
the moment a task was dispatched.

Add a `COPY extra/ extra/` in the node-builder stage, right before
`pnpm deps:download` runs, so the vendored binary is actually visible
to the script.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:11:23 +08:00
caorushizi d23f594b88 ci(docker): allow rebuilding an existing tag via workflow_dispatch
When a released image has to be re-cut (e.g. the broken 3.5.0 that
shipped with the stale per-downloader flags), there was no clean way
to re-publish under the same tag from the workflow — only a fresh
git tag push would trigger a build.

Extend `workflow_dispatch` with two inputs:

- `tag`        — image tag to publish under (e.g. `3.5.0` to overwrite
                 the existing one, or `3.5.0-fix.1` for a side-by-side).
                 Empty value falls back to the pre-existing
                 `dev-<run_id>` behaviour.
- `push_latest` — when rebuilding a stable release manually, optionally
                  re-stamp the `latest` tag at the same time.

`docker/metadata-action` already emits the standard OCI labels,
including `org.opencontainers.image.revision` sourced from `github.sha`.
That field alone is enough to tell two same-tag builds apart via
`docker inspect` — no Dockerfile changes needed, no manually-computed
build args. Summary step now reads version / commit from metadata-action's
own outputs (`steps.meta.outputs.version`) and `github.sha`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 14:36:30 +08:00
caorushizi b1cbb74f32 fix(docker): use --deps-dir and move CMD flags into entrypoint script
The image was failing at startup with:

    flag provided but not defined: -m3u8-bin

mediago-core consolidated the per-downloader `--*-bin` flags into a
single `--deps-dir` in 3.5.0-beta.1, but the Dockerfile was still
passing the old five flags (`--m3u8-bin`, `--bilibili-bin`,
`--direct-bin`, `--mediago-bin`, `--ffmpeg-bin`). Go flag parsing
rejects unknown flags, so the container exited immediately.

While fixing, extracted the invocation into
`scripts/docker-entrypoint.sh` rather than leaving a nine-line
quoted-array `CMD` at the bottom of the Dockerfile:

- `exec mediago-core …` replaces the shell so SIGTERM from
  `docker stop` reaches the Go process directly — no shell parent to
  swallow signals.
- `"$@"` forwards any extra flags from `docker run image … --foo=bar`
  so callers can override individual options without replacing the
  whole command.
- Switching from `CMD` to `ENTRYPOINT` makes the script the fixed
  launch path; runtime overrides still work via the forwarded `"$@"`.
- Editing default args no longer touches the Dockerfile (and thus
  doesn't invalidate the full image layer).

New `.gitattributes` pins `*.sh` to LF line endings so a Windows
checkout doesn't turn the entrypoint into a `/bin/sh^M: bad interpreter`
error inside the Linux container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 14:24:50 +08:00
caorushizi f01cae6554 docs: rewrite README (zh/en/jp) around user-facing features, extract dev setup
The three READMEs had drifted into a dev-first shape — the first screen
of each started with pnpm / install commands before telling end users
what the product actually does — and the Japanese version had fallen
behind (outdated commands, no v3.5.x downloads, screenshots pointing
at an external CDN rather than the in-repo images).

Rework all three around the v3.5.0 user-friendly feature story and
move the developer setup into its own file:

- Unified skeleton across zh/en/jp: header badges → one-sentence pitch →
  six feature sections (browser extension · YouTube / 1000+ sites ·
  OpenClaw Skill · HTTP API · in-app format conversion · Docker +
  LAN) → screenshots → v3.5.0 downloads (+ BT Panel one-click) →
  v3.5.0 highlights → tech-stack badge row → acknowledgements →
  disclaimer → link to CONTRIBUTING.
- Header elements preserved verbatim: nav links, language switcher,
  MediaGo Pro / Try Now banners, downloads / stars / forks / GitCode /
  Trendshift badges, 4 screenshots, BT Panel one-click deployment,
  the full 5-point disclaimer.
- Tech-stack prose list compressed to a single row of shields.io
  badges (React / Electron / Vite / TypeScript / Tailwind / shadcn/ui /
  Go / Ant Design).
- Acknowledgements gain aria2 (now powering direct downloads after the
  gopeed swap).
- README.jp.md specifically: fixed four `static.ziying.site/images/*`
  references to use the in-repo `./images/*` (matching zh/en); replaced
  the outdated v3.0.0-only download block with v3.5.0; added the
  English-language switcher link that was missing from the header.
- README.md/zh.md: dropped the v3.0.0 changelog block (history lives
  on GitHub Releases) and the "Quick Start" / "Running locally" pnpm
  command blocks.

New file: `CONTRIBUTING.md`. English only — prerequisites, repo layout,
a short list of the commands contributors actually type (deps:download
/ dev:electron / dev:server / pack:electron / release:electron / check),
Conventional Commits note, PR guidance. Each README ends with a one-
line pointer to it in the local language.

No product behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 14:14:28 +08:00
caorushizi 74b67101ae feat(extension): pin manifest version to the Desktop app version
Build & Push Docker Image / Build and push multi-arch image (push) Failing after 1s
Users naturally expect the "Current version" label in chrome://extensions
to match the version shown inside the MediaGo window. Tie both to a single
source of truth: `apps/electron/app/package.json`, which Electron's build
pipeline (`apps/electron/tsdown.config.ts`) already reads and exposes as
`process.env.APP_VERSION`.

`manifest.config.ts` now imports the same file and:

- `version`   — stripped down to the numeric prefix (`split(/[-+]/)[0]`)
                so Chrome accepts it. Chrome's manifest parser only
                allows 1–4 dot-separated integers; SemVer pre-release
                or build suffixes (`-beta.2`, `+build.7`) get rejected
                at install time.
- `version_name` — keeps the full SemVer string. It's free-form, not
                used for update-ordering, and is what chrome://extensions
                displays when present.

Effect: bumping `apps/electron/app/package.json` once is enough to
re-stamp both the Desktop binary and the bundled extension manifest.
The extension's own `packages/mediago-extension/package.json` is a
private workspace package — its `version` field is unused by the
manifest and intentionally left alone to avoid a pointless sync step
on every release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 18:16:07 +08:00
caorushizi 3279f40541 v3.5.0 2026-04-21 18:16:07 +08:00
caorushizi 62331e5c7b fix(ui,shared): refresh download list on externally-created tasks
Go Core already broadcast a `download-create` SSE event whenever
`/api/downloads` POST lands (UI, browser extension, Docker clients —
any route). The UI listened, but only to bump the sidebar badge via
`useDownloadStore.increase()`. The SWR list fetched by `useTasks`
stayed stale until the user refreshed manually, so tasks imported
through the extension's desktop-http / docker-http modes never
appeared without a page reload.

Fan the same SSE event through the existing `dispatchDownload`
pipeline so `useTasks` can mutate its SWR cache alongside the other
lifecycle events (success / failed / stopped).

- `packages/shared/common/src/types/index.ts`: new
  `DownloadCreatedEvent` (`type: "created"`, carries `{ ids, count }`).
- `apps/ui/src/api/events.ts`: existing `download-create` handler now
  additionally calls `dispatchDownload({ type: "created", ... })`
  after its badge-increment side effect. Parses `ids` from the
  payload for downstream filtering if anyone needs it later.
- `apps/ui/src/hooks/use-tasks.ts`: adds `isCreatedEvent` type guard
  and calls `mutate()` on hit, same pattern as the other events.

Internal task creation (UI form submit) is untouched — it still
refreshes via its own local mutate. The SSE path only matters for
tasks that show up out-of-band.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:11:37 +08:00
caorushizi d9c3d8ae1d fix(ui): let users copy text from the download-log terminal
Two separate bugs had to be fixed together before copy worked:

1. `terminal-dialog.tsx` used to swallow every contextmenu event
   inside the dialog (`e.preventDefault(); e.stopPropagation()`) —
   killing Electron's built-in "Copy" menu for selected text in the
   xterm log. Drop the preventDefault so the native menu fires, but
   keep stopPropagation (now scoped to the contextmenu handler
   alone) so the event doesn't bubble up through the React tree to
   `DownloadTaskItem`'s `onContextMenu` and pop its "select /
   download / refresh / delete" menu instead. Radix Dialog portals
   the DOM to document.body, but React synthetic events still
   travel the virtual tree — this is the classic portal-bubbling
   gotcha.

2. xterm.js does not bind Ctrl+C / Cmd+C to "copy selection" by
   default; it forwards them as control chars. Since the log view
   uses `disableStdin: true`, hijacking those keys is safe.
   `attachCustomKeyEventHandler` now intercepts copy shortcuts,
   writes the selection to the clipboard via `navigator.clipboard`,
   and returns false so xterm stops handling the event. No selection
   → passthrough.

Result: both right-click → Copy and Ctrl/Cmd+C paths work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:11:16 +08:00
caorushizi c34812841e fix(core): sanitize filenames at task creation to prevent path-separator injection
Twitter-style page titles like "(2) 主页 / X" land a literal "/" in
the download filename. Gopeed used to strip such characters silently;
aria2 is strict and passes them through to the OS, which then reads
"/" as a path separator — the save ends up at
`.../(2) 主页 /X-....mp4` pointing at a non-existent sub-directory
and fails with `ERROR_PATH_NOT_FOUND (errNum=3)`.

Sanitize once, at the task-creation boundary, so the DB row, the
downloader command-line `-o` arg, and the post-download
`CheckFileExists(rec.Name, ...)` probe all agree on the same
filesystem-safe value.

- New exported `core.SanitizeFilename` replaces reserved path /
  wildcard characters (`\ / : * ? " < > |`) and ASCII control chars
  with `_`, and right-trims dots / spaces (Windows strips those
  silently, producing a filename that doesn't match the DB row).
  Falls back to "download" if every character was illegal.
- `service/download_task.go` `AddDownloadTask` and `AddDownloadTasks`
  run titles through `SanitizeFilename` before the
  `FindByName` de-duplication check, so both the dedup lookup and
  the persisted row see the cleaned value.
- `core/downloader.go` `buildArgs` still calls `SanitizeFilename`
  defensively on `p.Name` — cheap, and guards any future path that
  bypasses the service layer.

Applies to every downloader (aria2, yt-dlp, BBDown, N_m3u8DL-RE,
mediago) since the fix is in the shared `name` arg-building branch
used by all Schema entries. Pre-existing broken tasks in the DB will
still fail "file not found" on the UI and need to be deleted +
re-created.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:10:54 +08:00
caorushizi 28e2c58366 fix(node-service): fail fast when the preferred port is already bound
If a stray MediaGo instance (typically the installed Desktop build
running in the background from the Start menu) already holds the
preferred port, a freshly-spawned child fails to bind and exits
immediately — but `waitForHealthy()` happily answers green because
the *other* instance's `/healthy` responds on the same port.
ServiceRunner then considers the service "started" and every
subsequent request silently lands on the wrong process. In practice
this surfaced as dev mode talking to the installed Go Core and
reporting a stale gopeed binary path, even though the dev build had
already been rebuilt with aria2c.

Pre-check: before spawning, open and close a throwaway TCP listener
on `<bindHost>:<preferredPort>`. If the bind fails, throw a
human-readable error instead of proceeding. The tiny check-vs-bind
race window is acceptable for catching the real-world "long-running
stray instance" case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:10:30 +08:00
caorushizi a9e3ed6a22 feat(core): switch direct downloader from gopeed to aria2
Gopeed's last release is aging and its SChannel-linked Windows build
chokes on modern CDN TLS handshakes (we hit SEC_I_MESSAGE_FRAGMENT on
twimg.com). Replace it with aria2 — same short flags (-x/-s/-k carry
over verbatim), stricter arg handling, and well-trodden cross-platform
builds.

Binaries are vendored in-tree at extra/aria2/<os>/<arch>/ rather than
pulled from a single GitHub release, because aria2 static builds for
Linux / macOS / Windows come from different upstream repos. To keep
the deps pipeline unified:

- `scripts/download-deps.ts` grows a "source": "local" branch that
  copies from `extra/<tool>/<os>/<arch>/` into `.deps/<os>-<arch>/`
  instead of fetching from GitHub. Resulting layout matches the rest
  of the tools, so binaryResolver.ts needs no changes.
- `scripts/deps-versions.json`: `gopeed` entry removed, `aria2`
  entry added with source:"local" + path:"extra/aria2".
- `apps/core/internal/core/types.go`: BinaryNames[TypeDirect]
  `"gopeed"` → `"aria2c"`.
- `apps/core/internal/core/schema/loader.go`: direct schema's Args
  map to aria2's flags (-d / -o, plus --console-log-level=notice /
  --summary-interval=1 / --allow-overwrite=true /
  --auto-file-renaming=false for parseable output and predictable
  rerun behaviour). ConsoleReg regexes updated to aria2's summary
  line format (e.g. "DL:512KiB" for speed, "(83%)" for percent).
  `--check-certificate=false` tacked on as a workaround for the
  SChannel handshake issue on the bundled 1.19.0 Windows build —
  proper fix is upgrading the vendored binary to a build that links
  against OpenSSL (e.g. 1.37.0), comment calls that out.
- `apps/core/.env`, `Dockerfile`, `apps/electron/scripts/build.ts`,
  `apps/core/README.md`: gopeed → aria2c / aria2 references.

No DB migration: existing `type: "direct"` rows keep working, the
underlying binary just swaps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:09:35 +08:00
caorushizi 48aabc9a7d v3.5.0-beta.2 2026-04-21 14:26:34 +08:00
caorushizi b72b39ad7e fix(node-service): bind to 0.0.0.0 when internal=false so localhost reaches the server
ServiceRunner used to pass the detected LAN IPv4 as the `HOST` env var
when `internal: false`, so the Go Core bound *only* to that LAN IP —
requests to 127.0.0.1 / localhost were refused. That blocked the
browser-extension's `desktop-http` mode (fixed at `http://127.0.0.1`).

Split bind host from display host:
- Bind (`HOST` env to child): `0.0.0.0` when `internal: false`, else
  `127.0.0.1`. Listener now accepts traffic on every interface.
- Display (`state.host` / `getURL()`): keeps the detected LAN IPv4
  when `internal: false` so the settings UI can surface a shareable
  LAN URL, falls back to 127.0.0.1 otherwise.

Net effect: Desktop is reachable via 127.0.0.1, localhost, and its LAN
IP simultaneously, while the UI still shows the LAN address.

Also translates all remaining Chinese JSDoc / inline comments in
`index.ts` and `utils.ts` to English, matching the rest of the package.
Behaviour otherwise unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:56:22 +08:00
caorushizi f04989e973 feat(extension): bundle into desktop app with settings entry and docs
Ship the MediaGo browser extension alongside the Desktop installer so
users can "Load unpacked" it from Chrome / Edge without building the
repo or downloading a separate zip. Adds a discovery path from inside
the app, a three-language doc page, and fixes the Desktop HTTP port
the extension was pointing at.

Packaging:
- `pnpm build:electron` now also builds `@mediago/extension` (turbo
  filter), and `apps/electron/scripts/build.ts` copies the dist to
  `app/build/extension/` and declares it in electron-builder
  `extraResources` → installers land `resources/extension/`.

Runtime:
- `resolveExtensionDir()` in binaryResolver — dev points at the
  monorepo dist, prod at `process.resourcesPath/extension`, env
  override via `MEDIAGO_EXTENSION_DIR`.
- New IPC `app.getExtensionDir()` returns the resolved path. UI pairs
  it with the existing `shell.open()` (no dedicated open-folder IPC)
  so the pattern matches configDir / binDir / localDir.

Settings UI:
- New "Browser extension directory" button in Settings → More Settings,
  right next to the existing folder shortcuts. Web/server mode hides
  the row behind `isWeb` and the stub returns "".
- New i18n key `extensionDir` in shared zh/en resources; duplicate
  `currentVersion` key removed from zh.ts along the way.

Port fix:
- Extension `DESKTOP_HTTP_BASE` corrected from `:9900` → `:39719` to
  match the Electron-side `preferredPort`. 9900 is the standalone
  Go Core / web-server port — two separate deployments.

Docs:
- New `docs/extension.md` + `docs/en/extension.md` + `docs/jp/extension.md`
  covering what the extension does, how to install the unpacked
  build, the three dispatch modes (schema / desktop-http / docker-http),
  import-behaviour toggles, language switching, and common pitfalls.
- Sidebar entries registered in `.vitepress/config.ts` for all three
  locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:56:05 +08:00
caorushizi e5bcd7a224 chore(core): translate build-script console logs to English
Mirror the English-only convention already used in the Electron/UI
scripts. Covers dev.ts (start dev server / build Player UI / compile
dev binary) and release.ts (build all-platform binaries / package /
clean). Behaviour unchanged — message text only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:55:34 +08:00
caorushizi fbf31d706c style(extension): apply Cursor-inspired warm palette and typography
Reskin popup + options to match Cursor's design language — warm off-
white over cream surfaces, gothic/serif/mono three-voice typography,
oklab borders, diffused atmospheric shadows. Preserves every feature;
only visuals change.

Palette & tokens (globals.css):
- Light: `#f2f1ed` page, `#26251e` text, `#e6e5e0` card, `#cf2d56`
  crimson for hover, `#f54e00` orange accent, surface-100..500 scale,
  AI-timeline colors (peach / sage / blue / lavender)
- Dark: warm-offset variant (`#1e1d16` page, `#ebeae5` text) — no
  cold neutral inversion
- Borders: `oklab(0.263084 -0.00230259 0.0124794 / 0.1)` perceptually
  uniform warm brown
- Shadows: ambient (16/8px), elevated (28/70px + oklab ring), focus
  (4/12px) — all token-driven
- Radius scale: 4px inline / 8px default / 10px featured / 14px breathing
- Motion: `--transition-color: 150ms ease` / `--transition-shadow: 200ms`

Fonts (locally bundled variable woff2, no remote fetch):
- Geist Variable (→ CursorGothic analogue)
- Source Serif 4 Variable (→ jjannon analogue)
- JetBrains Mono Variable (→ berkeleyMono analogue)
- OpenType feat utilities: `.font-feat-display` (ss09/ss01) and
  `.font-feat-editorial` (cv11/liga)
- Global h1/h2/h3 letter-spacing follows Cursor's size curve
  (-0.03em / -0.02em / -0.0125em)

Components:
- Button — warm-primary (hover → crimson text) + dark / outline /
  secondary-pill / tertiary-pill / ghost / light-surface / link
- Badge — full pill + timeline-color variants mapped to DownloadType
  (thinking/grep/read/edit/mediago) via `variantForDownloadType()`
- Card — ambient by default; `elevated` for permanent lift;
  `interactive` eases ambient → elevated on hover (used on ServerCard,
  the page's primary action target)
- Sonner / Switch / Input / RadioGroup — warm-palette rewrites with
  oklab focus rings and serif descriptions where appropriate

Pages:
- Popup — tonal bands (header cream → page-info surface-100 → list
  bg → footer surface-100), source type badges colored by DownloadType
- Options — responsive hero (40 → 52 → 60px with scaled letter-
  spacing), decorative blur glows behind headline, "cream → bg →
  cream" section rhythm with a closing surface-200 strip, per-card
  mono chapter dots in timeline colors, RuleListCard redone as a
  vertical timeline echoing Cursor's AI-step visualisation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:16:43 +08:00
caorushizi b782329ae2 feat(extension): add i18n with system/zh/en language toggle
The MediaGo extension shipped Chinese-only (50+ hardcoded strings across
popup, options, background). Wire up i18next + react-i18next so the UI
follows the browser language by default and lets the user override to
zh / en in a new LanguageCard on the options page.

- `ExtensionSettings.language: "system" | "zh" | "en"` (default system),
  persisted via existing `chrome.storage.local`
- `resolveLanguage()` maps system -> `chrome.i18n.getUILanguage()` or
  navigator.language with a zh/en fallback
- Bootstrap one i18next instance per page (popup / options), subscribe
  to `chrome.storage.onChanged` so switching on one surface updates the
  other without a reload
- Background service worker emits `LocalizedMessage` descriptors
  (`{ key, values? }`) for toast copy; popup calls `t(key, values)` so
  the SW never has to carry an i18n instance
- Full zh/en coverage for popup, options, error paths
- Chrome-native `_locales/{en,zh_CN}/messages.json` + `__MSG_*__` for
  `manifest.name` / `description` / `action.default_title` so the
  chrome://extensions row and toolbar tooltip follow the browser UI
  language (independent dimension from the in-UI toggle, by design)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:16:03 +08:00
caorushizi 65e20638ff feat(extension): expose silent / downloadNow knobs in options
The useUrlInvoke hook at apps/ui/src/hooks/use-url-invoke.ts reads
both `silent` and `downloadNow` query params; they were previously
hard-coded on the extension side. Surface them as user toggles so
Schema-mode users can opt into "open the download form for review"
and both modes can opt into "start downloading immediately instead
of just queuing".

- ExtensionSettings gains `downloadNow` (default false) and
  `schemaSilent` (default true).
- importViaHttp threads `startDownload` into the POST body.
- buildTaskDeeplink writes `silent=1` / `downloadNow=1` into the
  deeplink URL based on the flags (falsy => omit the key, matching
  the truthy-check in useUrlInvoke).
- New headless Switch component (no @radix-ui/react-switch dep).
- New ImportBehaviourCard in options, with a patch-style save hook
  so each toggle persists immediately.
- use-options.ts now merges on top of the current persisted settings
  so the server-config card doesn't wipe the new fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:58:44 +08:00
caorushizi 19048f6ead feat(extension): add MediaGo browser extension for media URL sniffing
A Manifest V3 extension that sniffs downloadable video / audio URLs
across every site the user visits and hands captured sources to a
MediaGo server in one click. Ships unlisted (load-unpacked .zip), no
Chrome Web Store.

Dispatch modes (user-picked in options, no silent fallback):
- desktop-schema: navigates the current tab to
  `mediago-community://index.html/?n=1&silent=1&url=…` using the
  cat-catch `chrome.tabs.update` pattern. Reuses the existing
  `useUrlInvoke` hook in apps/ui — no new deeplink plumbing.
- desktop-http: POST http://127.0.0.1:9900/api/downloads against the
  Go Core bundled in a running Desktop.
- docker-http: same POST against a user-configured host, with an
  optional X-API-Key header for `--enable-auth` deployments.

UI is React 19 + Tailwind v4 + shadcn/ui (new-york, neutral), matched
to apps/ui's stack. Popup shows per-tab sources with a red badge count;
options page has a 3-mode radio + per-mode field panel.

Supporting changes:
- Abstract sniff filter rules into @mediago/shared-common/sniff and
  point the Electron sniffing helper at the same exports so desktop
  and extension stay in lock-step.
- Tighten the YouTube host rule to actual video / short / live / embed
  URLs (drops the homepage and feed false-positives).
- Fix the macOS electron-builder config: CFBundleURLSchemes was
  hard-coded to a test string "mediagoaaa" — now sourced from
  process.env.APP_NAME (mediago-community in .env) like everywhere
  else. Also add a top-level `protocols` entry for clarity.
- Vite build pulls APP_NAME from the repo root .env via loadEnv() +
  `define`, so the scheme name stays single-sourced.
- Root scripts: `pnpm dev:extension`, `pnpm build:extension`,
  `pnpm pack:extension` (+ scripts/pack-extension.ts that cross-
  platform-zips dist/ into release/).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:54:31 +08:00
caorushizi b29a5f1748 perf(settings): mount cards one per frame to break up navigation jank
DevTools profiling showed a ~319ms long task when switching to the
settings page, dominated by scripting inside node_modules — Ant Design
Form.Item registration and cssinjs style generation firing for ~25
fields in a single synchronous pass. Chunk loading was only ~9ms, so
earlier fixes around lazy-import and Suspense fallbacks missed the real
cause.

Changes:
- Stream the 6 cards in one per animation frame (visibleCount driven
  by requestAnimationFrame), so the longest task is a single card's
  registration instead of the full page. The first card paints
  immediately and the rest follow over the next few frames.
- Skip the initial setFieldsValue via an isFirstSync ref;
  initialValues already seeds the form, so that effect was only
  forcing Ant Form to diff every Form.Item again right after mount.
- Wrap cardSections in useMemo with a narrow dep list
  (settings.apiKey, settings.local, envPath, updateAvailable, stable
  callbacks) so SSE config-changed updates don't rebuild the 340-line
  JSX tree unless those specific fields changed.
- Move the key from <Card> to the outer <div> so React stops treating
  each re-render as a remount of the card subtree.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 06:03:33 +08:00
caorushizi 8783d949ba feat(favorite): optional title with auto-fetch and URL-first form
- Make favorite title optional in both the DTO (drop binding:"required")
  and the add-favorite modal (no validator). Server falls back to the
  URL when the title is empty so the list entry always has a label.
- Auto-fill the title via the existing GET /api/url/title scraper when
  the user leaves the URL field, unless they already typed their own
  title. Failures are swallowed (server fallback still applies).
- Reorder the modal so URL is on top and Title below, matching the
  actual data-flow: users paste a URL first and the title follows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 05:28:35 +08:00
caorushizi bee6bd1cbd fix(ui,core): drive download badge via SSE and polish favorite error UX
- Go Core now broadcasts a "download-create" SSE event after
  POST /api/downloads succeeds, carrying the new task ids and count.
  The renderer's events.ts listens for it and calls useDownloadStore's
  increase() imperatively, so the sidebar badge updates regardless of
  which WebContents issued the request — the source-extract overlay
  dialog has its own Zustand instance and can no longer silently swallow
  the increment.
- Remove the now-redundant local increase() calls in download-form and
  browser-view-panel so the counter is incremented exactly once per task
  from a single source of truth.
- Favorite.Create returns 409 Conflict with a translated
  MsgURLAlreadyExists message when the URL already exists, instead of
  500 with the raw "url_already_exists" string.
- http.ts response interceptor now surfaces the server's translated
  message on 4xx/5xx instead of Axios's generic
  "Request failed with status code XXX".
- Simplify getFavIcon: drop the 1s <img> probe that dropped most URLs
  and return the canonical /favicon.ico; <Avatar> already falls back to
  a link icon when the image fails to load.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 05:28:22 +08:00
caorushizi bdea15ace9 fix(electron): rebuild tray menu on language change
The tray context menu was built once at startup, before the language
was loaded from Go Core, so its labels were stuck in the fallback
locale and never updated when the user switched languages. Keep the
Tray instance on the class, extract the menu build into refreshTrayMenu,
and subscribe to i18n "languageChanged" so the menu follows the
current language for both startup hydration and runtime changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 04:01:20 +08:00
caorushizi 72acd2ca94 refactor(ui,electron): fix "system" language fallback and rework settings layout
- Resolve AppStore.language at apply-time via a shared resolveAppLanguage
  helper so both renderer (navigator.language) and Electron main
  (app.getLocale) follow the actual OS locale instead of silently
  falling back to zh when the stored value is "system".
- Settings page: horizontal form with a fixed label column, borderless
  cards flowing naturally in a 2-column CSS columns layout; drop
  width="xl" on inputs and wrap button groups to fix English-label
  overflow.
- scripts/download-deps: add a PowerShell fallback for zip extraction
  on Windows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 03:46:38 +08:00
caorushizi ba5f389dbe fix: upgrade zustand to stable and fix broken player build scripts
- Upgrade zustand from 5.0.0-rc.2 to ^5.0.0 (resolved 5.0.12)
- Fix player:dev and player:build scripts that referenced non-existent
  @mediago/player-build, now correctly point to @mediago/core-build

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 04:22:05 +08:00
caorushizi aaa5ee69d2 docs: fix documentation drift in CLAUDE.md
Correct 8 inaccuracies where CLAUDE.md diverged from the actual codebase:
server is a Node.js launcher not Koa 3, apps/player/ doesn't exist,
remove non-existent commands, fix adapter layer and event bridge file
names, correct DI and module format claims.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:21:14 +08:00
dependabot[bot] fb671d5790 chore(deps): bump axios from 1.14.0 to 1.15.0
Build & Push Docker Image / Build and push multi-arch image (push) Failing after 0s
Bumps [axios](https://github.com/axios/axios) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.14.0...v1.15.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 00:30:13 +08:00
caorushizi e32bfd1cb2 v3.5.0-beta.1 2026-04-13 00:27:24 +08:00
caorushizi 50a1a90e6a docs: rewrite README in English and polish UI translations
- Rewrite README.md in idiomatic English as the default language
- Move original Chinese README to README.zh.md, delete README.en.md
- Polish ~80 UI translation strings in en.ts for natural English
- Fix hardcoded Chinese in Skills setup commands (setting-page)
- Fix mismatched translation key: reppeatPassword → repeatPassword

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:19:14 +08:00
caorushizi 2406b5b617 chore(electron,server): remove verbose startup logs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:19:14 +08:00
caorushizi 913d645d89 fix(electron): resolve deprecation warnings and unhandled promise rejection
- Replace deprecated webContents.canGoBack/goBack with navigationHistory API
- Add try/catch to autoUpdater.checkForUpdates to handle ERR_CONNECTION_REFUSED
- Replace cross-fetch with Node native fetch to fix url.parse() deprecation
- Remove unused cross-fetch dependency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:19:14 +08:00
caorushizi c7dd925a34 feat(core): add yt-dlp plugin and consolidate binary paths into --deps-dir
- Add youtube download type with yt-dlp schema (CLI args, progress regex)
- Replace 6 individual --xxx-bin flags with single --deps-dir flag
- Add BinaryNames map in Go core to auto-resolve binary paths from deps dir
- Simplify Electron/Server binary resolvers to return depsDir only
- Add yt-dlp to deps-versions.json (v2026.03.17)
- Add YouTube option to UI download form with URL auto-detection
- Fix download-terminal.tsx writing [object Object] instead of log content
- Improve error messages when binary not found (include path and type)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:19:14 +08:00
caorushizi c289d3690f refactor(browser-extension): remove dead FloatButton, jQuery and Lit dependencies
FloatButton was effectively dead — it listened for "webview-link-message" which
no one sends after the IPC namespace refactoring, and its functionality is fully
covered by BrowserViewPanel in the main UI. Remove it along with jQuery, Lit and
nanoid dependencies. Consolidate remaining BilibiliButton into a single-file
vanilla custom element, replace fragile global-index DOM lookup with
closest()-based card-relative queries, and replace setInterval polling with
MutationObserver. Build output reduced from 607KB to 1.9KB.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 00:19:14 +08:00
caorushizi 8224c68f66 refactor(ui): optimize source-extract page and browser navigation flow
Extract shared navigation logic into useBrowserActions hook, fix race conditions
in sniffing helper, add event listener cleanup in webview service, split store
selectors for granular re-renders, memoize list items, throttle ResizeObserver,
and add loading/error states to favorites list. Also remove unused getMachineId
IPC handler and fix IPC callback type signatures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 00:19:14 +08:00
caorushizi 6571fc022d refactor(ipc): namespace IPC events and consolidate similar methods
Replace flat IPC string constants with namespaced groups (browser.*,
app.*, dialog.*, shell.*, contextMenu.*, update.*) and consolidate
similar operations into generic methods:
- dialog.open/save replaces selectFile, selectDownloadDir, export/import
- shell.open replaces openDir, openUrl, openBrowser
- contextMenu.show replaces 3 separate context menu handlers
- on/off replaces rendererEvent/removeEventListener

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 00:19:14 +08:00
caorushizi 109c09a911 refactor(electron): replace screenshot hack with overlay WebContentsView for download dialog
Instead of capturing a screenshot, transferring a large DataURL via IPC,
and hiding the browser WebContentsView, use a separate overlay
WebContentsView layered above the browser view to render the download
form. This eliminates flicker, avoids potential page state loss, and
removes the inefficient screenshot transfer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 00:19:14 +08:00
caorushizi 90d7fe578f feat(core): upgrade mediago-core to v0.2.1 and adapt log parsing
- Bump mediago-core from v0.1.0 to v0.2.1 in deps-versions.json
- Update ConsoleReg to match new structured log format:
  speed regex for B/s units, start/isLive/error patterns
- Remove --no-log flag so structured logs are visible for parsing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:19:14 +08:00
caorushizi 82f948cdd9 fix(ui): fix openDir using wrong field names and antd 6 form warnings
- Use correct GoEnvPath fields (configDir/binDir) instead of non-existent workspace/binPath
- Defer form.setFieldsValue after modal opens so Form is mounted
- Add Input child to hidden Form.Item with name prop to satisfy antd 6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:19:14 +08:00
caorushizi 25d149e9ef feat(core): integrate mediago-core as new download type
Add mediago-core (caorushizi/mediago-core) as a fourth download type
alongside m3u8, bilibili, and direct. This enables HLS/DASH streaming
downloads using the custom Go-based downloader.

Changes span the full integration path: dependency download config,
Go Core backend (type, schema, CLI flag, binary map), binary resolution
for Electron/Server, shared TypeScript types, i18n labels, UI dropdown,
dev scripts, and Dockerfile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:19:14 +08:00
caorushizi 8d757ba18d docs: fix docker image tag (remove v prefix)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 22:20:01 +08:00
caorushizi 4f1c9fe65c docs: add OpenClaw Skill to README intro, docs homepage and guides
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:17:23 +08:00
caorushizi 78e3f96e3a docs: update README and guides for v3.5.0-beta.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:10:53 +08:00
caorushizi 0c377b7cb8 chore: bump version to 3.5.0-beta.0
Build & Push Docker Image / Build and push multi-arch image (push) Failing after 0s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:56:51 +08:00
caorushizi c2db365729 fix(core): add User-Agent and Referer for Bilibili title fetching
Bilibili returns an error page ("出错了") when requests lack a proper
User-Agent. Add browser User-Agent and Referer headers to both
GetPageTitle (service/helpers.go) and fetchTitle (handler/util.go).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:12:42 +08:00
caorushizi 7e21097175 fix(core): prioritize CLI --local-dir over appStore value
When --local-dir is explicitly passed (e.g. Docker's /app/mediago/downloads),
use it directly and write back to appStore. Previously appStore's stale value
(e.g. home directory) would override the CLI flag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:45:56 +08:00
caorushizi 0a36e39156 fix(core): whitelist static assets and SPA routes in auth middleware
Docker mode with --enable-auth was returning 401 for the homepage
and all static assets. Add /assets/, /favicon.ico, / and SPA
frontend routes (non-API paths without dots) to the auth whitelist.

Verified locally: homepage 200, /signin 200, /api/config 401 without key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:34:16 +08:00
caorushizi d178fa4510 chore: update pnpm-lock.yaml
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:00:36 +08:00
caorushizi 8ca4d443c4 feat(skills,docs,ui): add OpenClaw Skill with docs and settings tab
Skills:
- Rewrite SKILL.md as OpenClaw Skill (remove scripts dependency)
- Guide users to install MediaGo + initialize config in one message
- Support Chinese and English natural language commands
- Remove mediago-api.sh (use curl directly, cross-platform)

Docs:
- Add OpenClaw Skill page (zh/en/jp) with install, config, usage guide
- Add to VitePress sidebar for all 3 languages

Settings UI:
- Add "Skills 设置" tab with install command + init command
- Electron mode: init shows URL only
- Docker mode: init shows URL + API key
- One-click copy buttons

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:54:59 +08:00
caorushizi a9075606ad feat(skills): add mediago video download skill for Claude Code
Create a Claude Code skill that wraps MediaGo's download API:
- SKILL.md with setup instructions, download workflow, and config
  management via natural language
- mediago-api.sh helper script for health check, download, status,
  list, and wait commands
- Config stored in ~/.mediago-skill.json (URL + API key)
- Auto-detects download type from URL (m3u8/bilibili/direct)
- Supports first-time setup via natural language configuration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 02:10:16 +08:00
caorushizi 05da1e1a75 fix(core): persist default download directory to config store
On first run, the CLI --local-dir value was used by the Go core
but never written to appStore. The UI reads the download directory
from /api/config (appStore), so it showed empty. Now writes the
resolved local-dir to appStore when the stored value is empty.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 01:35:48 +08:00
caorushizi b5e0456786 fix(ui): fix auth redirect using wrong field name from API
The auth status API returns { setuped: bool } but the frontend was
checking for non-existent fields (initialized, enableAuth). Fix to
use the correct field name so unauthenticated users are properly
redirected to the signin page in server mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 01:29:26 +08:00
caorushizi 218603ff9f refactor(video): use ID-based video streaming and fix playback issues
- Change video stream route from /videos/*filepath to /videos/:id
  (lookup file path from database by task ID instead of filename)
- Add mimeType field to Video API response so video.js knows the format
- Fix getVideoURL double-slash bug (/videos/... → //videos/...)
- Handle directory downloads: scan inside for first video file when
  CheckFileExists returns a directory (e.g. multi-part bilibili downloads)
- Remove unused videoRoot field and ServeVideo function
- Update player-ui PlaylistItem to pass full VideoItem object

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 01:22:15 +08:00
caorushizi 6f19d08a08 fix(electron): bind core service to LAN address for mobile access
Change internal flag from true to false so the Go core listens on
LAN IP instead of 127.0.0.1, enabling mobile player access via QR code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:52:11 +08:00
caorushizi 5079180a31 refactor(server): use APP_NAME from env for data root directory
Load dotenv-flow to read APP_NAME, use it to derive the server data
root directory (~/.${APP_NAME}-server/) instead of hardcoding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:46:44 +08:00
caorushizi b291e30b13 docs(docker,server): unify data paths under single root directory
Consolidate all persistent paths under one mountable root:
- Docker: /app/mediago/{data,logs,downloads} with single VOLUME
- Server dev: ~/.mediago-server/{data,logs,downloads}
- DB renamed from app.db to data/mediago.db for consistency
- Update docker run command in README and docs (zh/en/jp)
- Add disclaimer to README

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:40:11 +08:00
caorushizi 9428f52f7d chore(ci): upgrade Node.js from 20 to 24 LTS
Node.js 20 reached EOL in April 2026. Upgrade build Node.js
version to 24.14.0 LTS across all CI workflows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:12:47 +08:00
caorushizi 0be4e5b9e6 chore(ci): upgrade GitHub Actions to Node 24 compatible versions
Upgrade all actions to eliminate Node.js 20 deprecation warnings:
- actions/checkout v5 → v6
- actions/setup-node v4 → v6
- actions/setup-go v5 → v6
- actions/upload-artifact v4 → v6
- pnpm/action-setup v4 → v5
- docker/setup-buildx-action v3 → v4
- docker/login-action v3 → v4
- docker/metadata-action v5 → v6
- docker/build-push-action v6 → v7

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:09:47 +08:00
caorushizi 5f8b1442c9 refactor(service-runner): remove portfinder, use fixed port
Remove dynamic port finding via portfinder. ServiceRunner now uses
preferredPort directly as the fixed port. Electron mode uses port 39719.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:01:37 +08:00
caorushizi b6c0c64dfa fix(docker): add libicu for N_m3u8DL-RE .NET runtime dependency
N_m3u8DL-RE is a .NET application that requires libicu for
globalization support. Without it the process aborts immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:50:37 +08:00
caorushizi 548ca9cd46 fix(docker): fix build failures for UI, Go deps, and unzip
- Install unzip in Node builder for zip-format deps (BBDown, gopeed)
- Copy apps/electron/app/package.json needed by UI vite config
- Add second go mod download after full source copy to fix go.sum

Verified: docker build + run succeeds locally (healthy + player UI ok)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:33:35 +08:00
caorushizi 5d1dc7716f fix(docker): include all workspace package.json files for pnpm install
pnpm install --frozen-lockfile requires all workspace packages to be
present. The .dockerignore excluded apps/electron/ entirely, and the
Dockerfile was missing apps/core/ and apps/electron/ package.json.

- Keep apps/electron/package.json in build context (exclude only src/)
- Add COPY for apps/core/package.json and apps/electron/package.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:21:04 +08:00
caorushizi e9795e1667 docs: add macOS installation tips for Intel and Apple Silicon
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:15:22 +08:00
caorushizi 1dd39a6973 chore(docker): rewrite Dockerfile and CI for GHCR multi-arch builds
- Rewrite Dockerfile as 3-stage build (Node builder → Go builder → runtime)
- Use --platform=$BUILDPLATFORM for native Node/Go compilation (no QEMU)
- Cross-compile Go binary via GOOS/GOARCH for target architecture
- Add --platform flag to download-deps.ts for target-specific deps
- Map Docker TARGETARCH to Node arch naming (amd64 → linux-x64)
- Flatten deps directory structure in runtime image
- Rewrite build-server.yml to push to ghcr.io with docker/metadata-action
- Single job multi-arch build (linux/amd64 + linux/arm64) via QEMU
- Update .dockerignore to exclude electron, docs, build artifacts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:14:42 +08:00
caorushizi de3b805ea0 feat(core,player-ui): refactor video API to use database and fix playback
Refactor video player API from filesystem scanning to database-backed:
- Video list queries downloads with status="success" + file existence check
- Add GET /api/v1/videos/:id endpoint for playing specific video by task ID
- Add playerUrl to /api/env response (computed from request host)
- Whitelist player/video paths in auth middleware
- Fix video URL generation to preserve folder structure (relative path)
- Fix player-ui Vite base path and video URL resolution under /player/
- Add openUrl implementation to web platform stubs
- Player-UI supports ?id= query param to auto-play specific video
- Play button now passes task.id instead of filename

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:45:27 +08:00
caorushizi 452bbc3b7d refactor: merge player Go backend into core
Merge the standalone mediago-player binary into mediago-core. The core
now serves the player UI at /player/ via go:embed, and provides video
listing/streaming endpoints. Video root reuses the existing download
directory (local-dir) so no separate flag is needed.

Changes:
- Add internal/video package to core (handler, service, types)
- Embed player-ui assets via //go:embed in assets/embed.go
- Add SPA handler for /player/ path
- Register /api/v1/videos and /videos/* routes in core router
- core:build now builds player-ui before Go compilation
- Remove apps/player/ entirely (Go app, scripts, configs)
- Remove VideoServer from Electron, derive playerUrl from coreUrl
- Remove player binary management from server app
- Update CI workflow to remove player go.sum cache path

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:05:42 +08:00
caorushizi ae50e1d995 feat: add node service 2026-04-02 20:34:37 +08:00
caorushizi 7fd898a23a fix(core,player): quote args with spaces for Windows shell mode
Node.js spawn with shell:true joins args with spaces without quoting,
so `-s -w` becomes two separate args on Windows cmd.exe. Manually
wrap args containing spaces in double quotes before passing to spawn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:00:45 +08:00
caorushizi b7527a012a fix(core): fix Windows build by using array-based spawn for Go commands
Refactor core build scripts to use the same array-based spawn pattern
that fixed the player build (5349295d). The single-string command with
shell quoting caused ldflags parsing failures on Windows cmd.exe.

- Change runCommand to spawn(command, args) with shell only on Windows
- Remove Unix-only 2>/dev/null redirect in getVersion()
- Replace shell chmod glob with native Node.js chmodSync loop
- Add child process cleanup on SIGINT/SIGTERM

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:52:58 +08:00
caorushizi 5349295df8 fix(player): fix go build ldflags parsing on Windows
Split "-ldflags=-s -w" into separate "-ldflags" and "-s -w" args
so that "-w" is not misinterpreted as a go build flag when shell
mode is enabled on Windows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:05:27 +08:00
caorushizi 38a4f63269 fix(ci,player): fix Windows build and add deps download step
- Add shell option for Windows in player's runCommand to fix
  "spawn pnpm ENOENT" error (Windows needs shell:true to find .cmd)
- Add deps:download step to CI workflow so third-party tools
  (ffmpeg, BBDown, etc.) are available during packaging

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:52:09 +08:00
caorushizi a6eae611af fix(ci): add Go and swag setup to electron build workflow
The build was failing because:
- macOS runners don't have Go pre-installed (exit code 127)
- Ubuntu/Windows runners lack the swag tool (spawn swag ENOENT)

Add setup-go action and swag installation step before building.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:42:50 +08:00
caorushizi 6dbe57c625 chore: add binary paths to .gitignore after history cleanup
Prevent accidentally committing binary dependencies that were
removed from git history via git-filter-repo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:12:24 +08:00
曹儒士子 f567f4f4a9 Merge pull request #616 from caorushizi/feat/skills
chore: migrate from @typescript/native-preview to typescript ^6.0.2
2026-04-02 17:00:01 +08:00
caorushizi b36585b927 docs: update catcatch integration URL scheme for community edition
Update the custom protocol from mediago:// to mediago-community://
and encode the URL parameter to handle special characters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:58:34 +08:00
caorushizi 7c2a0474cd fix(ui): fix download progress polling and taskItem reference error
1. events.ts: progress polling was using /api/downloads/active which
   returns DB records without percent/speed fields. Changed to /api/tasks
   which returns TaskInfo with real-time progress data.

2. download-item.tsx: lint fix had renamed callback params from `task`
   to `taskItem` but missed updating all references in the function body,
   causing ReferenceError. Reverted param names back to `task`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:57:01 +08:00
caorushizi 8d4903bdff refactor(electron): use dynamic app name for custom URL scheme
Replace hardcoded "mediago" protocol scheme with dynamic `appName`
from environment, so community/dev builds use their own scheme
(e.g. "mediago-community://"). Applies to:

- constants: defaultScheme = appName
- main.window.ts: production URL uses defaultScheme
- browser.window.ts: same
- build.ts: CFBundleURLSchemes uses dynamic scheme

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:46:52 +08:00
caorushizi 7eb654b6a5 feat(core,ui): move password hashing to backend with bcrypt
Previously the frontend hashed passwords with MD5 + a hardcoded salt
before sending to the backend. This exposed the salt in client-side JS
and used an insecure hash algorithm.

Backend changes:
- Setup: accepts plain password, bcrypt hashes it, generates UUID apiKey
- Signin: accepts plain password, bcrypt verifies, returns stored apiKey
- Status: checks passwordHash instead of apiKey
- AppStore: new passwordHash field
- Middleware: add X-API-Key header support (was only reading Authorization)
- CORS: allow X-API-Key header

Frontend changes:
- Signin page: send plain password, receive apiKey from response
- Remove md5/crypto-js dependency and APIKEY_SALT_KEY constant
- api/download-task: auto-inject localPath/deleteSegments from store
  (was lost during go-adapter migration, caused EOF errors)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:27:09 +08:00
caorushizi 403e01b28a fix(ui): auto-inject apiKey from Zustand store on every request
Instead of manually calling setHttpApiKey() after signin, use an axios
request interceptor that reads apiKey from useAppStore.getState() on
every request. This ensures the header is always in sync with the
stored apiKey, regardless of when or how it was set.

Removed: setHttpApiKey() function (no longer needed).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:21:44 +08:00
caorushizi da5fa76c04 fix(ui): call setHttpApiKey after signin to fix 401 errors
After login, the apiKey was saved to Zustand/localStorage but never
set on the http axios instance header, causing subsequent API calls
to return 401.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:18:20 +08:00
caorushizi b2fa4d2fbc feat(ui): add response types to all api/ functions and SWR hooks
Add proper TypeScript return types to every api/ function, using
existing shared types (Favorite, Video, Conversion, AppStore, etc.)
from @mediago/shared-common.

New types added:
- GoEnvPath (api/config.ts) for /api/env response
- AuthStatus (api/auth.ts) for /api/auth/status response

Updated SWR hooks to use typed data instead of Record<string, unknown>
casts: useFavorites, useConfig, useEnvPath, useAuthApi, useConversions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:10:05 +08:00
caorushizi c0bbfa5b09 fix(ui): fix auth redirect in server mode
Two issues:
1. http.ts 401 handler used window.location.hash (HashRouter syntax)
   but the app uses BrowserRouter — changed to window.location.pathname
2. useAuth() blindly checked localStorage apiKey, which is always empty
   on first visit. Now queries Go Core auth status first: only redirects
   to /signin when auth is enabled and configured (or needs setup)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:50:51 +08:00
caorushizi f6e59e864a fix(core): allow X-API-Key header in CORS config
The UI sends X-API-Key for authentication, but the CORS AllowHeaders
config only listed Authorization. This caused preflight failures in
server mode where UI (localhost:8555) and Go Core (127.0.0.1:9900)
are on different origins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:47:04 +08:00
caorushizi ee669f94ab refactor(electron,ui): improve sidebar extract toggle and cleanup
- Sidebar: toggle between embedded and external browser window mode,
  show extract page in sidebar even when opened externally
- Home page: remove redundant material extraction button
- Converter: temporarily disable video format options, keep audio only
- Electron: remove node-machine-id dependency from package.json
- Tool bar: fix combineToHomePage argument structure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:28:19 +08:00
caorushizi fa79482682 fix(ui): fix converter page pagination layout
PageContainer was missing flex-col, causing pagination to render inline
with the list. Add flex-col + gap-3 to PageContainer and remove
duplicate padding/bg from the inner list container.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:04:07 +08:00
caorushizi 69cd2e6394 feat(ui): add pagination to converter page
Replace hardcoded { current: 1, pageSize: 500 } with dynamic pagination
state. Default pageSize is 50 with an Ant Design Pagination component
at the bottom of the list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:00:24 +08:00
caorushizi b363b3dd7d refactor(electron): remove getMachineId controller method
machineId is now generated by Go Core on first startup (uuid),
so the Electron IPC handler using node-machine-id is no longer needed.

Removes: getMachineId handler, node-machine-id import, nanoid import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:53:33 +08:00
caorushizi 23fc28805b feat(core,ui): generate machineId in Go Core instead of Electron IPC
Move machineId generation from Electron (node-machine-id) to Go Core.
On first startup, if machineId is empty in appStore config, Go Core
generates a UUID and persists it. This makes machineId available in
both Electron and Web modes via the config API.

UI now reads machineId from getConfig() instead of getMachineId() IPC.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:50:36 +08:00
caorushizi ba9e22381b fix(ui): add unwrapResponse to usePlatform() IPC method returns
PlatformApi methods via Electron IPC return { code, data, msg } objects.
After migrating from useAPI() to usePlatform(), the auto-unwrap logic was
lost, causing consumers to receive raw objects instead of extracted data
(e.g. onSelectDownloadDir() returned [object Object] instead of a path).

Fix: wrap all PlatformApi function calls in the Proxy get trap to
auto-unwrap { code, data } responses, matching the old useAPI() behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:43:15 +08:00
caorushizi ec2f82db37 refactor(ui): replace monolithic useAPI() with domain-specific api/ + SWR hooks
Split the monolithic useAPI() hook into a clean layered architecture:

1. api/ layer: domain-specific axios request functions
   - download-task.ts, favorite.ts, conversion.ts, config.ts, auth.ts, util.ts
   - events.ts: native EventSource for Go Core SSE (no sdk dependency)

2. SWR hooks: thin wrappers with auto-caching and mutation
   - useFavorites(), useConversions(), useConfig(), useEnvPath(), useAuthApi()

3. usePlatform(): pure Electron IPC methods (Proxy-safe delegation)

4. http.ts: axios instance with setupHttp() + response interceptor

Go SSE events and Electron IPC events are now fully separated:
- Go SSE (download/config) → api/events.ts → onDownloadEvent/onConfigChanged
- Electron IPC (webview/update) → usePlatform() → addIpcListener

Deleted: use-api.ts, go-adapter.ts, go-event-bridge.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:39:55 +08:00
caorushizi 8f47bf93af feat: size 2026-04-02 00:15:20 +08:00
caorushizi 0bb8f93e94 fix(electron,core,ui): improve startup robustness and logging
- core: move defer logger.Sync() after logger re-init to avoid nil panic
  on early startup
- electron: replace console.log with ElectronLogger in DownloaderServer
  and ElectronApp for consistent log output
- ui: add 1s delay before fetching coreUrl in Electron mode to wait for
  Go core to finish starting
- ui: guard data?.list access in converter-page to prevent crash when
  data is undefined

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 23:12:00 +08:00
caorushizi 23108c342b fix(electron): ensure window always shows regardless of Go core startup errors
Previously, init() was not awaited in index.ts, so any failure in the async
chain (e.g. getConfig() timing out) was silently swallowed and the window
was never created.

Fix:
1. Await mediago.init() in index.ts so errors surface properly
2. Move serviceInit() (window creation) before Go core startup so the UI
   always appears, even if the backend fails to start

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 16:05:16 +08:00
caorushizi 43c11928fa feat(core,player): exclude swagger from prod builds; add chmod to binaries
1. Use Go build tag `dev` to conditionally compile swagger docs and routes.
   Production binaries no longer include swaggo/files (swagger UI assets):
   - mediago-core:   34 MB → 24 MB  (-10 MB, -29%)
   - mediago-player: 29 MB → 13 MB  (-16 MB, -55%)
   Dev builds (-tags dev) retain full swagger at /swagger and /docs.

2. Set executable permission (0o755) on built binaries (non-Windows).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 13:22:16 +08:00
caorushizi 4b01faad42 fix(electron): copy Go binaries via extraResources instead of npm packages
The packaged app failed to launch because mediago-core was missing from
Resources/bin/. The asarUnpack approach relied on platform-specific npm
optional deps that pnpm workspace hoisting never installed in app/node_modules/.

Fix: copy locally-built binaries (core, player, deps) into app/build/bin
and app/build/deps during the pack step, then expose them via extraResources
so electron-builder places them at the expected Resources/bin/ and
Resources/deps/ paths at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 12:56:35 +08:00
caorushizi 10beac128f fix: resolve TypeScript type errors 2026-04-01 12:26:48 +08:00
caorushizi 3f4dc0290e fix(lint): resolve all oxlint errors across codebase
- Merge duplicate 'node:child_process' imports (player/scripts/utils.ts)
- Remove unused catch parameter (ui/utils/tdapp.ts)
- Merge duplicate 'react' imports (tool-bar.tsx, source-extract/index.tsx)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:15:15 +08:00
caorushizi 38cb884b77 style: apply oxfmt formatting across codebase
Also fix pre-existing lint errors exposed by staging:
- Merge duplicate 'eventsource' imports in eventEmitter.ts
- Remove unused catch parameter in eventEmitter.ts
- Merge duplicate 'react' imports in app-side-bar.tsx

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:12:49 +08:00
caorushizi e659fccfad feat(converter): add file modal with browse, format, quality fields
Redesign the "Add File" flow as a modal dialog containing:
- File path field with "Browse" button (opens native file picker)
- Output format selector (Video: MP4/MKV/WebM, Audio: MP3/AAC/FLAC/WAV)
- Quality selector (High/Medium/Low)
- Two action buttons: "Add to List" (saves for later) and
  "Convert Now" (saves and starts conversion immediately)

Also fix silent failure when adding files — getFileName() used
new URL() which throws on local file paths. Replaced with simple
path.split() extraction and added try-catch error handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:04:12 +08:00
caorushizi 21e232f042 feat: implement format conversion with ffmpeg in Go Core
Move ffmpeg execution from Electron to Go Core so conversion works in
both Electron and web server modes.

Go Core backend:
- Add -ffmpeg-bin CLI flag to pass ffmpeg binary path
- Enhance Conversion DB model with status, outputPath, outputFormat,
  quality, progress, and error fields
- Create converter executor (service/converter.go) that spawns ffmpeg,
  builds args per format/quality, and parses stderr for progress
- Add StartConversion/StopConversion to service with SSE events for
  real-time progress (conversion-start/progress/success/failed/stop)
- Add POST /api/conversions/:id/start and /:id/stop endpoints

Supported formats:
- Video: MP4 (H.264), MKV (H.264), WebM (VP9) with CRF quality
- Audio: MP3, AAC, FLAC, WAV with bitrate quality presets
- Quality presets: high/medium/low mapping to CRF and bitrate values

Frontend (converter-page):
- Add format selector (Video/Audio groups) and quality dropdown
- Show per-item status badge, progress bar during conversion
- Action buttons: Start/Stop/Open Folder/Delete based on status
- "Convert All" batch button for pending items
- SWR auto-refresh during active conversions

Integration:
- Pass -ffmpeg-bin from Electron and core:dev to Go Core
- Remove old broken Electron-only convertToAudio code
- Add startConversion/stopConversion to core-sdk and GoApi
- Add i18n keys for new UI elements (en/zh)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:38:14 +08:00
caorushizi e4aa62e6e4 fix: use function form for manualChunks (Vite 8 compat)
Vite 8's rollup types no longer accept the object shorthand for
manualChunks. Switch to the function form which is compatible with
all Vite versions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 05:16:57 +08:00
caorushizi 36daf13822 perf(ui): optimize frontend performance for UI and player
UI app:
- Add Vite manual chunks (vendor, antd, zustand) for better caching
- Wrap DownloadTaskItem with React.memo() to prevent list re-renders
- Wrap IconButton with React.memo() to prevent re-renders in lists
- Add missing Suspense fallback to SigninPage route

Player app:
- Add Vite manual chunks (videojs, vendor) to isolate video.js bundle
- Remove duplicate resize calculation in usePlayerSize hook
  (ResizeObserver already handles container size changes)
- Disable SWR revalidateOnFocus/revalidateOnReconnect for video list
  (list only changes when user adds/removes videos)
- Extract PlaylistItem into memoized component to prevent re-renders
  when sibling items change, and eliminate duplicated rendering logic

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 05:14:13 +08:00
caorushizi 55833142bd perf: only poll /api/tasks while downloads are active
Previously the app polled GET /api/tasks every second unconditionally
from startup, even with no downloads running. Now polling is driven
by SSE events: started on download-start, stopped when no tasks have
Downloading status (checked after success/failed/stop events).

Applied to both Electron main process (downloader.server.ts) and
web/UI mode (go-event-bridge.ts).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 05:06:06 +08:00
caorushizi 350e02c0f0 chore: translate all Chinese comments to English
Converted all Chinese-language comments across 47 source files to
English, covering Go backend (apps/core, apps/player), TypeScript
scripts, Electron main process, UI components, and shared packages.

Only comment text was modified — code, string literals, log messages,
and i18n translation values were left untouched.

Also fix pre-existing lint errors in scripts/release.ts and
scripts/utils.ts (node: protocol prefix, unused imports, bare catch).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 03:56:57 +08:00
caorushizi f2d150bb02 fix: prevent settings from reverting to defaults after restart
Three root causes fixed:

1. ElectronStore migration deleted Go Core's config.json on every startup.
   The old migration code unconditionally ran unlinkSync() on config.json
   in the workspace directory — the same file Go Core uses for persistence.
   Removed the migration entirely since it has already run for all users
   (window-state.json now holds the bounds data).

2. core:dev mode saved config to the wrong directory (log dir instead of
   data dir) because -config-dir was not passed. Added config_dir to
   devConfig and the dev() command args, pointing to ~/.mediago/data to
   avoid collision with the old v2.0 ~/.mediago/config.json format.

3. syncCLIToAppStore() overwrote user settings with CLI default values
   on every Go Core startup in core:dev mode. Removed the function; CLI
   args now only set initial cfg defaults, while appStore (user config)
   takes precedence via the existing syncAppStoreToCfg().

Also aligns blockAds default (true) between Go Core and frontend Zustand store.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 03:08:18 +08:00
caorushizi fff1259704 fix: lazily resolve getGoApi() in useAPI to avoid init-time crash
getGoApi() was called eagerly inside useMemo during component mount,
before initGoAdapter() had run. Now GoApi methods resolve getGoApi()
at invocation time, so useAPI() can safely be called in App.tsx before
the adapter is initialized.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 01:23:42 +08:00
caorushizi 9470f357ec fix: restore IS_SETUP constant used as SWR cache key
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 01:22:40 +08:00
caorushizi c1b8d67849 chore: add vercel-react-best-practices skill
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 01:21:02 +08:00
caorushizi 52c3cdfc60 refactor: split MediaGoApi into GoApi + PlatformApi, remove Proxy routing
Major cleanup of the frontend adapter layer:

1. Type split: MediaGoApi (55 methods) → GoApi (22 data/CRUD methods via
   Go Core HTTP) + PlatformApi (~30 Electron-native methods via IPC)

2. Remove Proxy-based routing: replace the fragile apiAdapter Proxy +
   GO_METHODS set + ALL_API_METHODS array with direct object composition.
   GoApi methods never fall through to Electron IPC.

3. Clean dead code: remove 18 preload methods that called IPC channels
   with no handler (getFavorites, getAppStore, createDownloadTasks, etc.)
   Remove unused event constants (SOCKET_TEST, SETUP_AUTH, SIGNIN, etc.)

4. New platform-stubs.ts: explicit no-op stubs for web/server mode
   instead of a catch-all Proxy that returns empty responses.

5. Simplified useAPI: direct spread of wrapped GoApi + PlatformApi
   instead of dynamic Object.keys().reduce() enumeration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 01:17:48 +08:00
caorushizi 9156cf9cd9 fix: gate UI behind Go adapter init and auto-build core/player
- Add adapterReady state to App.tsx: show Loading until initGoAdapter
  completes, preventing API calls before goHandle is set (fixes
  "No handler registered" errors for get-favorites, get-app-store, etc.)
- Add error handling and debug logging for player server startup
- Prepend core:build && player:build to dev:electron, dev:server,
  pack:electron, and release:electron scripts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:52:38 +08:00
caorushizi 38fa71b44b fix: config persistence, download-now auto-start, and player integration
1. Sync Go Core config to Zustand on app startup so settings persist
   across restarts (Go Core is the single source of truth).

2. Go Core Create handler now respects startDownload param — downloads
   start immediately when "Download Now" is clicked.

3. Player integration fixes:
   - Electron: graceful skip when player binary is not built
   - Server: start player service alongside Go Core, expose playerUrl
   - Web adapter: derive playerUrl from current host instead of empty string

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:27:30 +08:00
caorushizi ce406767be docs: update CLAUDE.md with current architecture
Add apps/core, apps/player, apps/player-ui, packages/core-sdk.
Remove deleted packages (shared/node, shared/browser, config).
Replace stale TypeORM references with @mediago/service-runner.
Document Go Core commands, adapter architecture, and port numbers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:27:18 +08:00
caorushizi 8a5ac39cc5 chore: migrate player-ui to oxlint and upgrade to React 19, Vite 8, TS 6
- Remove biome.json from apps/player (use root oxlint/oxfmt config)
- Remove eslint from apps/player-ui, switch lint script to oxlint
- Upgrade React 18 → 19, Vite 7 → 8, TypeScript 5.9 → 6.0
- Upgrade @vitejs/plugin-react 5 → 6, @types/react to v19
- Fix vite.config.ts: __dirname → import.meta.dirname (ESM compat)
- Fix tsconfig.app.json: remove deprecated baseUrl option

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:05:02 +08:00
caorushizi 906db36c81 chore: set author to caorushizi and license to MIT across all packages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 23:18:16 +08:00
caorushizi 208f2095bf chore: update app name to mediago-community and disable mac signing
- Rename APP_NAME to mediago-community in .env
- Update APP_ID to mediago.caorushizi.cn
- Set mac identity: null to skip code signing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 23:18:04 +08:00
caorushizi e6a8a76d32 chore: upgrade electron to v41 and related build tools
- Upgrade electron 35.7.5 → 41.1.0
- Upgrade electron-builder 26.0.12 → 26.8.1
- Upgrade electron-updater ^6.6.2 → ^6.8.3
- Add electron-builder-squirrel-windows 26.8.1 to resolve peer conflict
- Remove rebuild script (electron-rebuild no longer a dependency)
- Remove rebuild:electron script from root package.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 23:03:16 +08:00
caorushizi 41eda86917 chore: update deps, remove showTerminal web restriction, format code
- Bump knip to v6.1.1, turbo to v2.9.1
- Remove oxlint-tsgolint devDependency
- Remove hidden={isWeb} from showTerminal setting item
- Format .vscode/settings.json and setting-page/index.tsx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 22:53:42 +08:00
caorushizi ffd3170261 chore: remove unused native electron devDependencies
@electron/rebuild and prebuild-install are no longer needed as the
Electron app has no native runtime dependencies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 22:43:45 +08:00
caorushizi 2425598c6a fix: fix download log display corruption in terminal
- Fix PTYRunner.readPTYOutput to use raw chunk reads instead of a
  broken bufio.Scanner split that called onStdLine with the entire
  remaining buffer on every invocation, causing log lines to be
  written multiple times
- Remove unused flushInterval field and NewPTYRunnerWithInterval
- Add convertEol: true to XTerm so bare \n in stored logs is treated
  as \r\n, preventing misaligned output when replaying log files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 22:43:36 +08:00
caorushizi e413bb5f38 fix: update binaryResolver paths, deps versions, and clean up docker-empty
- Fix monorepo root resolution in electron binaryResolver
- Downgrade N_m3u8DL-RE to v0.3.0-beta, use gopeed-web assets
- Remove win32-arm64 ffmpeg entry
- Convert download-deps to ESM with explicit file reads
- Add tsx devDependency
- Remove unused .docker-empty/.gitkeep

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:50:52 +08:00
caorushizi 93432c5968 refactor: move player-ui from apps/player/ to apps/ for cleaner monorepo structure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:49:19 +08:00
caorushizi a0c10de673 fix: update server binaryResolver and remove npm binary deps
- Update apps/server/src/binaryResolver.ts to use monorepo paths
  instead of npm platform packages
- Remove @mediago/core and @mediago/deps from server package.json
- Add .gitkeep placeholder in apps/player/assets/ui/ for go:embed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 21:20:36 +08:00
caorushizi ffc4940508 feat: update cross-references and add deps download script
Phase 4-6 of monorepo merge:

- Create scripts/download-deps.ts for downloading third-party tools
  from GitHub Releases (ffmpeg, N_m3u8DL-RE, BBDown, gopeed)
- Add scripts/deps-versions.json for version-locked tool definitions
- Update pnpm-workspace.yaml for new monorepo structure
- Change @mediago/core-sdk references from file:// to workspace:*
- Remove @mediago/core, @mediago/deps, @mediago/player npm deps
  from electron package.json
- Rewrite binaryResolver.ts to use monorepo paths (dev) and
  extraResources (production) instead of npm platform packages
- Simplify apps/core/scripts/config.ts (remove npm config, use .deps/)
- Add Go project scripts to root package.json
- Update turbo.json build outputs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 21:17:42 +08:00
caorushizi 72241bffd4 chore: merge mediago-player and clean up
- Subtree add mediago-player into apps/player
- Remove npm/ directory (7 platform packages)
- Remove pnpm-workspace.yaml, pnpm-lock.yaml, turbo.json
- Rename ui/ to player-ui/ to avoid conflict with apps/ui
- Rename package to @mediago/player-build
- Rename player-ui package to @mediago/player-ui
- Simplify gulpfile.ts and tasks.ts (remove npm build/publish)
- Remove npm templates
- Remove turbo, biome, vite override from devDependencies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 21:13:11 +08:00
caorushizi b6d3f80f03 Merge commit '4c93a1bdfaae8a96cfac8594921c4354a03ab531' as 'apps/player' 2026-03-31 21:10:53 +08:00
caorushizi 4c93a1bdfa Squashed 'apps/player/' content from commit 102bd03c
git-subtree-dir: apps/player
git-subtree-split: 102bd03cac286179ab06c3527edea045023e6fb1
2026-03-31 21:10:53 +08:00
caorushizi 85c0ff01e8 chore: clean up mediago-core after subtree merge
- Move SDK to packages/core-sdk
- Remove npm/ directory (14 platform packages no longer needed)
- Remove .bin/ directory (third-party binaries, will download from GitHub)
- Remove pnpm-workspace.yaml, pnpm-lock.yaml
- Remove npm.ts build script and templates
- Simplify gulpfile.ts (remove npm build/publish tasks)
- Rename package to @mediago/core-build

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 21:10:39 +08:00
caorushizi a9129f4a3c Merge commit '84ed412899931d87a526bb0e8d391a9b9f60ce18' as 'apps/core' 2026-03-31 21:08:13 +08:00
caorushizi 84ed412899 Squashed 'apps/core/' content from commit 55da68f3
git-subtree-dir: apps/core
git-subtree-split: 55da68f3dc6415a2cefb53e6112d0e6e4c77f9be
2026-03-31 21:08:13 +08:00
caorushizi e5138360d8 chore: add go.work and update .gitignore for monorepo merge
Prepare for merging mediago-core and mediago-player into the monorepo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 21:07:32 +08:00
caorushizi e6441aae6d refactor: remove DownloadController and fix export/import file dialogs
Delete DownloadController (4 methods now handled by Go Core go-adapter),
move showDownloadDialog to WebviewController. Remove exportFavorites,
importFavorites, exportDownloadList from GO_METHODS so Electron mode
uses IPC with native file dialogs instead of bypassing them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 20:04:47 +08:00
caorushizi a611919bcb refactor: remove shared-browser, shared-node, config packages
- shared-browser: inline createBrowserI18n into apps/ui/src/i18n/
- shared-node: move code into apps/electron by function:
  - i18n → core/i18n.ts
  - handle decorator → core/decorators.ts
  - registerControllerHandlers → core/registerControllerHandlers.ts
  - TYPES → types/symbols.ts
  - binaryResolver → utils/binaryResolver.ts
  - DownloaderServer/VideoServer → services/
  - copy binaryResolver to apps/server for resolveCoreBinaries
- config: move tsconfig.{base,app,node}.json to monorepo root

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 19:47:51 +08:00
caorushizi c0cf028c55 feat: fetch real env paths from Go Core for config/bin directories
Update go-adapter getEnvPath to call the new /api/env endpoint instead
of returning hardcoded empty strings for binPath and workspace.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 19:13:23 +08:00
caorushizi d3142586f7 fix: resolve multiple runtime issues in web mode
- Add notification badge update after creating download tasks
- Add defensive null check in useAPI to prevent destructuring undefined adapter results
- Import download store for increase() calls in download-form

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 18:47:02 +08:00
caorushizi 8e8e71bd6b feat: replace Node.js server with Go Core direct connection
- Remove old Koa/Socket.IO server, replace with minimal ServiceRunner launcher
- UI now connects directly to Go Core via SDK (HTTP + SSE), no middleware
- Add GoEventBridge for SSE events and progress polling
- Add Dockerfile and docker-compose for single-container deployment
- Fix web mode adapter Proxy ownKeys for proper method enumeration
- Fix auth flow: setupAuth now sets apiKey, App.tsx passes stored apiKey
- Add error handling in download-form for getVideoFolders
- Remove conversion.controller import (file was deleted)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 17:30:44 +08:00
caorushizi 1a58a05ce4 fix: forward download failed/stopped events to UI for immediate status updates
Failed and stopped events were only logged but never sent to the UI,
causing tasks to stay stuck showing "downloading" status. Now all terminal
state changes (success/failed/stopped) are forwarded via IPC/Socket.io
and trigger SWR revalidation for immediate UI refresh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 14:55:23 +08:00
caorushizi 8c70945a40 feat: migrate controllers to Go backend API, remove TypeORM/dao/services layer
Remove the entire TypeORM-based data layer (entities, repositories, services)
and refactor Electron/Server controllers to delegate to Go backend via HTTP API.
Add go-adapter for UI to communicate directly with Go core service.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 14:33:11 +08:00
caorushizi 515bbe9b8e feat: migrate config to Go backend as single source of truth
Electron:
- Add GoConfigCache for synchronous config reads, seeded from Go API
- Replace all ElectronStore AppStore reads with GoConfigCache
- SSE config-changed events drive platform side effects + IPC to UI
- Slim ElectronStore to window bounds only, with old config.json migration
- Fix useProxy handler passing boolean instead of proxy address
- Add missing audioMuted/enableMobilePlayer SSE handlers

Server:
- Add ServerConfigCache to eliminate per-request getConfig() HTTP calls
- Migrate auth middleware and controllers to use config cache
- Remove StoreService and conf dependency entirely

UI:
- Replace "store-change" full-dump IPC with incremental "config-changed"

Shared:
- Remove appStoreDefaults (no longer needed)
- Fix DownloadStatus.Watting → Pending typo
- Fix VideoServer.enableMobilePlayer inverted logic
- Simplify DownloadServiceOptions (Go reads its own config)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 14:28:50 +08:00
caorushizi 0e6b86b3a0 chore: remove unused .biomeignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 20:03:47 +08:00
caorushizi b2bcfebeea chore(ui): add design assets
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 20:02:55 +08:00
caorushizi cc81040641 chore: optimize Turborepo task configuration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 18:24:24 +08:00
caorushizi 84102b4b00 docs: add CLAUDE.md for Claude Code guidance
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 18:16:54 +08:00
caorushizi 3a565676e3 chore: migrate from Prettier to oxlint + oxfmt toolchain
Replace Prettier with oxfmt for formatting and expand oxlint config with
full rule categories, React/TS overrides, and per-app config inheritance.
Add husky + lint-staged for pre-commit hooks to incrementally enforce
formatting on changed files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 17:51:09 +08:00
caorushizi 8f1df0c008 chore: switch from rolldown-vite to official vite 8 and upgrade plugin-react
- vite: npm:rolldown-vite@latest → ^8.0.3
- @vitejs/plugin-react: ^5.0.4 → ^6.0.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 17:09:54 +08:00
caorushizi e6c58bc2b7 chore: upgrade react to v19 and antd to v6
- react/react-dom: ^18.3.1 → ^19.2.4
- @types/react: ^18.3.3 → ^19.2.14
- @types/react-dom: ^18.3.0 → ^19.2.3
- antd: ^5.27.4 → ^6.3.4
- @ant-design/icons: ^6.1.0 → ^6.1.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 17:06:28 +08:00
caorushizi 217bdde7d8 chore: migrate from @typescript/native-preview to typescript ^6.0.2
Replace @typescript/native-preview with standard typescript package
and remove tsgo-specific dts config from tsdown configurations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 16:57:48 +08:00
曹儒士子 299bd25c69 Refactor Footer.vue template structure 2026-02-24 14:42:53 +08:00
caorushizi deb3f9ff67 feat: banner 2026-01-16 02:08:48 +08:00
tiyongliu f9418857bd feat: 实现linux dark主题系统顶部topbar的颜色没随设置改变。 2026-01-07 17:28:48 +08:00
zhecydn 6294502641 Update tampermonkey.md
docs: add front matter for VitePress layout
2026-01-07 17:23:51 +08:00
zhecydn 336147166c Create tampermonkey.md
Create tampermonkey.md
2026-01-07 17:23:51 +08:00
caorushizi 9406d8c2cb v3.1.0-beta.1
Make MediaGo Docker Image / Build Docker Image (arm64, linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Make MediaGo Docker Image / Create Multi-Arch Manifest (push) Has been cancelled
Make MediaGo Docker Image / Build Docker Image (amd64, linux/amd64, ubuntu-latest) (push) Failing after 1s
2025-11-19 00:29:40 +08:00
caorushizi 99ceba5363 feat: true portable
fixes: #476
2025-11-19 00:21:08 +08:00
caorushizi 7dca0a1c81 feat: 支持修改每页的数量 2025-11-18 22:56:12 +08:00
Charlie 8fc92801ce fix: 嗅探自动更新,如果页面title变化,更新页面信息,展示到右侧嗅探列表 (#587)
Co-authored-by: 曹儒士子 <84996057@qq.com>
2025-11-18 21:59:30 +08:00
y2hlbg 506cd12fba fix: 增加归一化逻辑,解决进度条倒退问题 2025-11-18 21:56:24 +08:00
caorushizi 343c41a862 feat: 优化 schema 调起的逻辑 2025-11-18 21:51:07 +08:00
caorushizi a3f0658cf0 fix: null window error 2025-11-18 18:45:56 +08:00
caorushizi e33d695ca9 feat: 添加 sentry 2025-11-18 18:31:46 +08:00
Charlie cfe0644962 Merge pull request #585 from caorushizi/fix/issues_464
fix: 代理支持socks5
2025-11-18 17:04:16 +08:00
y2hlbg bc64464e20 fix: 代理支持socks5 2025-11-18 16:30:34 +08:00
caorushizi 3d4ae46e30 fix: 构建 dev 版本的 docker 2025-11-18 13:06:17 +08:00
caorushizi 0ff360e7d6 fix: download progress
fixes: #577
2025-11-18 12:16:00 +08:00
caorushizi 0ad5f54ed3 add gitcode badge 2025-11-07 08:07:21 +08:00
caorushizi 4a560a0f63 fix: docker image id
Make MediaGo Docker Image / Build Docker Image (arm64, linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Make MediaGo Docker Image / Create Multi-Arch Manifest (push) Has been cancelled
Make MediaGo Docker Image / Build Docker Image (amd64, linux/amd64, ubuntu-latest) (push) Failing after 1s
2025-11-05 00:50:58 +08:00
caorushizi 7aa22d096f v3.1.0-beta.0 2025-11-04 23:56:21 +08:00
caorushizi 9202051ff4 feat: update @mediago/deps 2025-11-04 23:55:07 +08:00
caorushizi 9e9748c6f8 fix: docker 无法播放视频 2025-11-03 18:18:47 +08:00
caorushizi ec94a136d6 feat: update deps to 0.0.5 2025-11-03 17:33:37 +08:00
caorushizi cb961bdc2f feat: 添加到 docker 2025-11-03 17:05:16 +08:00
caorushizi 45f740c36f fix: docker build 2025-11-03 16:31:22 +08:00
caorushizi 993c8f73ed fix: docker build 2025-11-03 16:02:07 +08:00
caorushizi a52518be28 fix: docker build 2025-11-03 15:21:32 +08:00
caorushizi 4c22bbdc18 fix: 修复批量下载字体大小 2025-11-03 12:00:01 +08:00
caorushizi 18cb7709de fix: docker build 2025-11-03 11:50:09 +08:00
caorushizi c1bda7fa9c fix: electron build 2025-11-03 11:41:12 +08:00
caorushizi b55dd91320 fix: pnpm setup 2025-11-03 11:35:28 +08:00
caorushizi 9c2f4664d6 fix: electron build 2025-11-03 11:26:05 +08:00
caorushizi 2467088b2f fix: electron builder 2025-11-03 11:10:34 +08:00
caorushizi 53d59c3ab9 feat: 修复登录时的问题 2025-11-03 03:50:35 +08:00
caorushizi e174aeef6a fix: 登录的一些问题 2025-11-03 03:50:35 +08:00
caorushizi f490539ee2 feat: 完善登录逻辑 2025-11-03 03:50:35 +08:00
caorushizi 74b3826319 refactor: 优化目录结构 2025-11-03 03:50:35 +08:00
caorushizi 910d1c30a6 feat: 添加登录页面 2025-11-03 03:50:35 +08:00
caorushizi c5f2dc94c9 fix: mac tray icon 2025-11-02 20:56:39 +08:00
caorushizi 4cd1955235 fix: electron build 2025-11-01 17:30:41 +08:00
caorushizi 16809c4ab5 fix: server build 2025-11-01 16:30:51 +08:00
caorushizi 52e40fed54 fix: server build 2025-11-01 16:23:35 +08:00
caorushizi bd833e7590 fix: build script 2025-11-01 15:49:48 +08:00
caorushizi f26248f0df feat: update deps 2025-11-01 15:46:05 +08:00
caorushizi 1fe1df486f feat: update electron 2025-11-01 01:55:42 +08:00
caorushizi 6189ca7f9a feat: 优化图标 2025-11-01 01:55:42 +08:00
caorushizi 9c5cdfff0d feat: 优化 ts 类型 2025-11-01 01:55:42 +08:00
caorushizi 7423e5cfca feat: 优化 ts 类型 2025-11-01 01:55:42 +08:00
caorushizi b9a4f5db57 feat: 优化代码逻辑 2025-11-01 01:55:42 +08:00
caorushizi 5717f454e6 fix: 优化代码逻辑 2025-11-01 01:55:42 +08:00
caorushizi efae7de938 fix: 修复了一些问题 2025-11-01 01:55:42 +08:00
caorushizi 1ea3c02471 fix: mediago server log 2025-11-01 01:55:42 +08:00
caorushizi cece6ee89b fix: something 2025-11-01 01:55:42 +08:00
caorushizi 02233e15fb build(server): switch Dockerfile to multi-stage 2025-11-01 01:55:42 +08:00
caorushizi 9e0752cafc chore(repo): convert text files to CRLF line endings 2025-11-01 01:55:42 +08:00
caorushizi f92f1affc6 feat(download-log): fetch logs lazily in dialog
- replace terminal drawer with modal dialog and load logs via SWR

- resolve ffmpeg binary path using deps module across runtimes

- flatten shared i18n resources and bump core packages
2025-11-01 01:55:42 +08:00
caorushizi 9e0869b675 feat: update server 2025-10-28 01:18:57 +08:00
caorushizi b09b0a300e feat: build release 2025-10-28 00:35:31 +08:00
caorushizi 2fe71f39c7 fix: build 2025-10-28 00:35:31 +08:00
caorushizi 77b08f33c2 feat: use @mediago/core 2025-10-28 00:35:31 +08:00
caorushizi 919c872106 feat: use oxlint 2025-10-28 00:35:31 +08:00
caorushizi 02c33e468d fix: download task 2025-10-28 00:35:31 +08:00
caorushizi c1a84b25a3 feat: update @mediago/core v0.0.10 2025-10-28 00:35:31 +08:00
caorushizi c5251c1b3e feat: update @mediago/player 2025-10-25 00:16:46 +08:00
caorushizi 675b277f17 feat: update @mediago/core 2025-10-25 00:16:46 +08:00
caorushizi c673363848 fix: turbo.json 2025-10-25 00:16:46 +08:00
caorushizi 44ca432508 feat: use tsdown 2025-10-25 00:16:46 +08:00
caorushizi 9d0e663928 feat: use tsdown 2025-10-25 00:16:46 +08:00
caorushizi 438ddfb46b feat: use tsdown 2025-10-25 00:16:46 +08:00
caorushizi 13077a571f feat: use tsdown 2025-10-25 00:16:46 +08:00
caorushizi 0482b6bbae feat: use tsdown 2025-10-25 00:16:46 +08:00
caorushizi 171e46cf1b feat: use tsdown 2025-10-25 00:16:46 +08:00
caorushizi 427e552ed7 fix: exists & color 2025-10-25 00:16:46 +08:00
caorushizi fa7a175260 fix: video not exist & download dropdown item 2025-10-25 00:16:46 +08:00
caorushizi 39d2646cd8 feat: use @mediago/service-runner 2025-10-25 00:16:46 +08:00
caorushizi dccc3db755 feat: use @mediago/core 0.0.5 2025-10-25 00:16:46 +08:00
caorushizi c452b14b2f fix: 获取下载进度 2025-10-25 00:16:46 +08:00
曹儒士子 cad18fafc0 refactor(ui): rename download item task variable 2025-10-16 14:58:02 +08:00
caorushizi 43160bf5e7 feat: modify database 2025-10-16 02:39:47 +08:00
caorushizi d97131d081 feat: add native bin 2025-10-16 00:51:56 +08:00
caorushizi 4cf87b7811 feat: ui 2025-10-16 00:51:56 +08:00
caorushizi fe7eae890b upgrade tailwindcss 2025-10-14 18:56:10 +08:00
caorushizi 174c11e792 update package.json 2025-10-14 18:56:10 +08:00
caorushizi 463664f7ec feat: use video and downloader server 2025-10-14 18:56:10 +08:00
caorushizi 59617b868f feat: add video service 2025-10-13 11:34:56 +08:00
caorushizi 5795d8cadd feat: remove mobile and desktop player 2025-10-13 11:34:56 +08:00
caorushizi 9323f54ac6 refactor: download form 2025-10-13 11:34:56 +08:00
caorushizi e733f13fde fix: __bin__ path 2025-10-13 11:34:56 +08:00
caorushizi 4a340617e6 tsconfig 2025-10-09 00:28:12 +08:00
caorushizi e1af2310f4 feat: edit tsconfig 2025-10-09 00:19:53 +08:00
caorushizi 395c3fac2c feat: use vite define version 2025-10-09 00:19:53 +08:00
caorushizi 26a402366d docs: package.json 2025-10-09 00:19:53 +08:00
dependabot[bot] 4290c8648b build(deps): bump axios from 1.11.0 to 1.12.2
Bumps [axios](https://github.com/axios/axios) from 1.11.0 to 1.12.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.11.0...v1.12.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.12.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-09 00:12:06 +08:00
caorushizi 467cc1da3a feat: opt deps 2025-10-08 22:51:34 +08:00
caorushizi 5dad2fb876 fix: build cache 2025-10-08 22:24:17 +08:00
caorushizi 3c78f377fc fix: build failed 2025-10-08 21:42:54 +08:00
caorushizi b869f23b20 fix: build failed 2025-10-08 21:00:03 +08:00
caorushizi 144ba39fdc fix: build failed 2025-10-08 20:47:00 +08:00
caorushizi ebb544cbcb fix: build failed 2025-10-08 20:27:51 +08:00
caorushizi 65be51094d fix: build failed 2025-10-08 18:06:23 +08:00
caorushizi ecc45895d0 fix: build failed 2025-10-08 17:57:56 +08:00
caorushizi 252a166978 feat: edit action script 2025-10-08 17:47:39 +08:00
caorushizi 49195f4206 feat: edit action script 2025-10-08 17:38:20 +08:00
caorushizi 0659964009 feat: edit action script 2025-10-08 17:35:29 +08:00
caorushizi 103a111191 feat: edit action script 2025-10-08 17:30:45 +08:00
caorushizi 1a45c685e7 feat: edit action script 2025-10-08 17:12:02 +08:00
caorushizi d6ae275bf3 feat: edit actions sctipts 2025-10-08 16:58:05 +08:00
caorushizi a51cf4e9b1 feat: add copilot instructions 2025-10-08 14:31:09 +08:00
caorushizi 0867e2da0f refactor: reponame 2025-10-08 14:30:53 +08:00
caorushizi 779172f953 feat: use vite 2025-10-08 04:48:59 +08:00
caorushizi c31bd974db feat: add linux arm64 binaries and harden download flows
- bundle linux arm64 helper binaries under bin/linux/arm64\n- harden frontend download and browser components with safer defaults\n- assign stable ids via useId to avoid duplicate DOM targets\n- align shared Favorite type with title/icon fields and update consumers\n- point release pipeline at backend-electron and force clean deletions\n- switch preload build entry and loosen biome static interaction lint
2025-09-26 13:34:22 +08:00
caorushizi 81a72bbc1a refactor(i18n): centralize internationalization system and migrate to dedicated packages
- Move i18n instance from shared-common to shared-node for backend usage
- Create shared-browser package with React i18n integration for frontend
- Centralize i18n resources in shared-common with structured organization
- Update all imports across backend-electron, backend-web, and frontend-main
- Add language detection and browser-specific i18n configuration
- Maintain backward compatibility while improving maintainability

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 00:01:35 +08:00
caorushizi fb380ffcc6 docs: add repository guidelines 2025-09-25 22:51:16 +08:00
caorushizi 15f3aecc9d refactor(frontend): rename files to kebab case 2025-09-25 21:06:11 +08:00
caorushizi c2c198c636 refactor: centralize IPC handle constants 2025-09-25 16:41:29 +08:00
caorushizi 75eed24552 refactor: remove shared package and migrate to specific shared packages
- Remove packages/shared package entirely
- Update all apps and packages to use @mediago/shared-common and @mediago/shared-node instead
- Simplify router implementation by removing dynamic electron imports
- Update build configurations and dependency references
- Clean up pnpm-lock.yaml to reflect new package structure

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 19:41:19 +08:00
caorushizi ad51d812d7 docs: 📸 migrate to local images and update architecture documentation
- Replace remote image URLs with local images in README.md
- Add screenshot images to images/ directory
- Update CLAUDE.md with current project structure and commands
- Remove obsolete @ghostery/adblocker patch

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 17:22:31 +08:00
曹儒士子 33f893f961 refactor: extract download state aggregator 2025-09-24 16:07:33 +08:00
曹儒士子 67631c995a Refactor app store handler mapping 2025-09-24 16:06:52 +08:00
曹儒士子 fb45213073 refactor: share controller registration helper 2025-09-24 16:05:18 +08:00
曹儒士子 e0671bd4c8 refactor: share app store defaults 2025-09-24 13:50:39 +08:00
spencer 80e493e1ae refactor: add item to ignore the '.store' directory 2025-09-16 16:52:23 +08:00
spencer c19d3b1fd7 refactor: use DESC sorting of created date in the download list and download complete list page 2025-09-13 20:16:33 +08:00
caorushizi d57c67eca6 refactor: 🚀 simplify download API by consolidating methods with optional parameters
Consolidate download-related methods to reduce duplication:
- Merge downloadNow, downloadItemsNow into addDownloadItems with startDownload flag
- Merge editDownloadNow into editDownloadItem with startDownload flag
- Remove redundant addDownloadItem method in favor of addDownloadItems
- Update both Electron and Web backends with unified parameter structure
- Maintain backward compatibility while simplifying the API surface

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 17:01:41 +08:00
caorushizi 4a1239b100 refactor: 📦️ split shared package into domain-specific packages
Extract shared package into three separate packages for better separation of concerns:
- @mediago/shared-common: Common types, constants, utilities, i18n
- @mediago/shared-node: Node.js specific services, DAOs, entities
- @mediago/shared-browser: Browser-specific code

Benefits:
- Improved dependency management and package isolation
- Eliminated circular dependencies between packages
- Better maintainability with domain-separated codebases
- Enhanced type safety across the monorepo

Changes:
- Created new packages with proper TypeScript configurations
- Migrated all code from packages/shared/src/{common,node,browser}
- Updated 40+ import statements across the entire codebase
- Resolved package dependency conflicts and circular references
- All backend packages pass TypeScript compilation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 17:01:41 +08:00
caorushizi cbe337baed refactor: 🏗️ extract shared business logic services from backend controllers
Extract platform-agnostic business logic from backend-electron and backend-web controllers
into reusable services in the shared package, enabling code reuse across platforms:

- ConversionService: handles conversion record management operations
- DownloadManagementService: manages download lifecycle, CRUD operations, and metadata
- FavoriteManagementService: handles bookmark/favorites management

Benefits:
- Eliminates code duplication between Electron and Web backends
- Centralizes business logic maintenance in shared package
- Preserves platform-specific UI interactions (dialogs, notifications)
- Maintains dependency injection architecture consistency

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 17:01:41 +08:00
caorushizi f6433e8f80 refactor: 🏗️ unify router architecture across Electron and Web backends
- Rename IpcHandlerService to ElectronRouter for clarity
- Introduce shared MEDIAGO_METHOD and MEDIAGO_EVENT constants
- Replace separate @get/@post decorators with unified @handle decorator
- Standardize routing metadata handling across platforms
- Add MediaGoRouter interface for consistent router contracts
- Remove unused @on decorator from Electron helpers

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 17:01:41 +08:00
caorushizi a6aa2094e2 fix: 🔧 make electron adapter enumerable with Proxy traps
- Move adapters from src/adapters/ to src/hooks/adapters/
- Add ownKeys and getOwnPropertyDescriptor traps to electronAdapter Proxy
- Enable Object.keys() iteration over electronAdapter methods
- Fix useAPI hook to properly enumerate API methods in Electron environment

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 19:55:46 +08:00
caorushizi a264284c05 refactor: 🏗️ migrate to Inversify 7 with binding decorators for dependency injection
Upgrade Inversify from v6 to v7 and implement modern binding decorators pattern:
- Replace manual container bindings with @provide decorators
- Update all controllers, services, and vendors to use decorator-based DI
- Simplify inversify.config.ts by removing explicit bindings
- Add @inversifyjs/binding-decorators dependency for cleaner configuration
- Maintain singleton pattern and existing functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 19:55:46 +08:00
caorushizi 0e898be77c refactor: 📦️ extract Electron preload script into standalone package
Extract preload.ts from apps/backend-electron to packages/electron-preload for better modularity and reusability. This includes:

- Create new @mediago/electron-preload package with proper build configuration
- Update backend-electron to use the new preload package dependency
- Maintain all existing functionality while improving architecture

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 19:55:46 +08:00
caorushizi bd6a974e5c refactor: 🏗️ restructure frontend API and IPC adapters
- Replace `apis/` folder with `adapters/` for clearer architecture
- Integrate IPC logic into corresponding adapter files:
  - `electronIpcAdapter` in `electron.ts`
  - `webIpcAdapter` in `web.ts`
- Add new `useAPI` Hook with cleaner separation of concerns
- Maintain backward compatibility with `useElectron` alias
- Improve code organization and maintainability

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 19:55:46 +08:00
caorushizi 5356a580f2 refactor: 🔧 unify TypeScript configurations across all packages
- Create base configurations in packages/config for different app types
- Standardize all tsconfig.json files to extend from shared configs
- Fix module resolution inconsistencies (NodeNext -> bundler mode)
- Remove duplicate configuration options across packages
- Improve code quality by fixing linting issues in shared services

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 19:55:46 +08:00
caorushizi 90218e876e docs: add docs 2025-09-05 19:55:46 +08:00
caorushizi 735dc4bbd5 refactor: 📦️ reorganize the file directory 2025-09-05 19:55:46 +08:00
caorushizi 3113617d26 refactor: 🔧 migrate ptyRunner to app-specific implementations
- Move ptyRunner from shared package to individual apps (electron/webapi)
- Update package.json names and dependency versions
- Standardize node-pty versions across apps
- Remove shared ptyRunner export and update imports
- Update DownloaderService to accept runner as parameter

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 19:55:46 +08:00
caorushizi e5fad91210 feat: 🐳 add Docker development environment for webapi
- Add Docker configuration to resolve node-pty native module conflicts
- Create docker/Dockerfile for production builds
- Create docker/Dockerfile.dev for development with hot reload
- Add docker-compose.yml for easy development setup
- Include system dependencies (python3, make, g++, libicu-dev) for native modules
- Update package.json with dev:docker and build:docker commands
- Move .dockerignore to root directory for proper build context
- Remove electron rebuild scripts as Docker handles native module compilation
- Enable mixed development: webapi in container, electron on host

Usage:
- pnpm dev:docker # Start webapi in Docker container
- pnpm -F electron dev # Start electron locally in parallel

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 19:55:46 +08:00
caorushizi 763a5fcd86 refactor: 📦️ rename apps and migrate to pure Turborepo build system
- Rename apps with more descriptive names:
  • main → electron (Electron main process)
  • renderer → frontend (React UI for both Electron and web)
  • backend → webapi (Koa.js API server)

- Remove tools/scripts custom build logic:
  • Replace custom zx scripts with native Turbo commands
  • Update dev command to use Turbo filters for plugin/mobile builds
  • Migrate web-release build to use Turbo pipeline
  • Remove tools/scripts directory entirely

- Update comprehensive README documentation for each app:
  • apps/electron/README.md - Desktop application lifecycle and system integration
  • apps/frontend/README.md - React UI shared between Electron and web
  • apps/webapi/README.md - Standalone API server with Docker support

- Update all configuration references:
  • package.json: Update all Turbo filter commands
  • biome.json: Remove tools/scripts path references
  • pnpm-workspace.yaml: Remove tools/* workspace
  • CLAUDE.md: Update architecture documentation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 19:55:46 +08:00
caorushizi ba36d89cef refactor: 🏗️ migrate to Turborepo monorepo structure
- Install Turborepo for build orchestration and caching
- Reorganize directory structure following monorepo best practices:
  - Move packages/main,renderer,backend,mobile,plugin → apps/
  - Keep packages/shared for shared utilities and types
  - Create packages/config for shared configurations
  - Move scripts/ → tools/scripts/
- Update pnpm-workspace.yaml for new structure (apps/*, packages/*, tools/*)
- Configure turbo.json with build pipelines and task dependencies
- Update all package.json scripts to use Turbo commands
- Update biome.json paths for new directory structure
- Add packageManager field to root package.json
- Update CLAUDE.md documentation with new architecture

Benefits:
- Faster builds with intelligent caching and incremental builds
- Parallel task execution across packages
- Clear separation between apps and shared packages
- Improved developer experience with unified build system

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 19:55:46 +08:00
caorushizi b5ecbbb748 refactor: 🔧 migrate from ESLint/Prettier to Biome
- Install @biomejs/biome as unified linting and formatting tool
- Remove all ESLint/Prettier configurations and dependencies
- Create unified biome.json with TypeScript, React, CSS support
- Update all package scripts to use Biome commands
- Configure lint-staged across all packages
- Add root-level lint, format, and lint:fix commands

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 19:55:46 +08:00
caorushizi 386c6bfe16 fix: 🐛 type error 2025-05-20 14:12:18 +08:00
曹儒士子 a792fdc0b1 Merge pull request #483 from caorushizi/refactor/main 2025-05-18 19:31:49 +08:00
caorushizi 4358f7b767 refactor: 📦️ optimize socket.io 2025-05-18 19:29:31 +08:00
曹儒士子 658b245baf Merge pull request #482 from caorushizi/refactor/main 2025-05-18 13:49:30 +08:00
caorushizi b6dcd6fac7 refactor: 📦️ refactor main packages 2025-05-18 13:47:50 +08:00
caorushizi 953f9032b9 refactor: 📦️ add @mediago/shared 2025-05-17 23:02:31 +08:00
caorushizi 60652fbcd0 feat: optimize the message update mechanism of the download list 2025-05-17 15:18:19 +08:00
caorushizi e8d5cf5c7a refactor: 📦️ reconstruct the video download service 2025-05-17 00:51:56 +08:00
曹儒士子 4b34f51d96 Merge pull request #467 from zhou-xh/zhouxh
feat: 增加小屏页面自适应
2025-04-24 17:47:30 +08:00
zhouxunhai 14a88a78ed feat: 增加小屏页面自适应 2025-04-24 14:00:17 +08:00
曹儒士子 d6a8992d04 Merge pull request #432 from caorushizi/fix/some-thing
feat:   support for pushing download links to docker
2025-03-08 21:25:27 +08:00
caorushizi 63ddc1457b feat: get the page title in the web download 2025-03-03 13:11:35 +08:00
caorushizi ca6a90ec63 feat: support for pushing download links to docker 2025-03-02 23:28:07 +08:00
曹儒士子 f7a2790285 Merge pull request #427 from caorushizi/fix/bilibili-name
fix: bilibili video name
2025-03-02 18:21:15 +08:00
曹儒士子 e62a591c24 Merge branch 'master' into fix/bilibili-name 2025-03-02 18:20:47 +08:00
Charlie 8e5089b8db refactor: getPageTitle useElectron 2025-03-02 15:44:43 +08:00
caorushizi f9cbb7dfd4 fix: 🐛 skip check segments count 2025-02-28 02:11:56 +08:00
caorushizi 4f5625f7b4 fix: 🐛 some bugs 2025-02-28 02:07:37 +08:00
Charlie fa2c8de385 fix: bilibili video name 2025-02-18 00:46:21 +08:00
Charlie 68fcbd1670 Merge pull request #426 from caorushizi/fix/bilibili-dom
fix: ts fix
2025-02-17 22:09:42 +08:00
Charlie c5a9398607 fix: ts fix 2025-02-17 22:07:16 +08:00
曹儒士子 6886bf7e06 Merge pull request #421 from caorushizi/fix/bilibili-dom
fix: bilibili ele bug fix
2025-02-17 01:15:42 +08:00
Charlie d1871a1bb2 fix: bilibili ele bug fix 2025-02-17 01:09:27 +08:00
曹儒士子 05407600d6 Merge pull request #420 from caorushizi/fix/macOs_scheme
fix: scheme open app in macOs
2025-02-15 00:11:09 +08:00
Charlie ba7e280b82 docs: comments English 2025-02-15 00:05:40 +08:00
Charlie 0857376da5 fix: scheme open app in macOs 2025-02-14 23:37:40 +08:00
曹儒士子 d39e1fb1c5 Merge pull request #419 from caorushizi/title-encode
fix: 🐛  url encode
2025-02-14 01:31:18 +08:00
caorushizi 9867d26bdd fix: 🐛 url encode 2025-02-14 01:29:23 +08:00
曹儒士子 e5f6d70b7c Merge pull request #418 from IOLOII/master 2025-02-13 17:00:48 +08:00
IOLOII 2b363a91a9 Revert "build(deps): bump react-dom and @types/react-dom"
This reverts commit c8c7351f9e.
2025-02-13 16:29:34 +08:00
曹儒士子 c79fba28d1 Merge pull request #413 from caorushizi/gopeed
feat:  straight link download
2025-02-12 01:08:25 +08:00
caorushizi 66b841697d feat: direct link download 2025-02-12 01:06:56 +08:00
caorushizi 98ee8d88f7 feat: straight link download 2025-02-11 03:56:27 +08:00
曹儒士子 b6a1328be4 Merge pull request #381 from caorushizi/dependabot/npm_and_yarn/i18next-browser-languagedetector-8.0.2
build(deps): bump i18next-browser-languagedetector from 8.0.0 to 8.0.2
2025-02-10 17:59:02 +08:00
曹儒士子 4f6636b9eb Merge branch 'master' into dependabot/npm_and_yarn/i18next-browser-languagedetector-8.0.2 2025-02-10 17:58:53 +08:00
曹儒士子 1a33e35fe4 Merge pull request #384 from caorushizi/dependabot/npm_and_yarn/socket.io-client-4.8.1
build(deps): bump socket.io-client from 4.8.0 to 4.8.1
2025-02-10 17:58:38 +08:00
曹儒士子 17a76f9db0 Merge branch 'master' into dependabot/npm_and_yarn/socket.io-client-4.8.1 2025-02-10 17:58:32 +08:00
曹儒士子 4cd50e54c1 Merge pull request #382 from caorushizi/dependabot/npm_and_yarn/multi-3739722284
build(deps): bump react-dom and @types/react-dom
2025-02-10 17:58:18 +08:00
曹儒士子 215b1a85d0 Merge branch 'master' into dependabot/npm_and_yarn/multi-3739722284 2025-02-10 17:58:06 +08:00
曹儒士子 deb73cdcce Merge pull request #400 from caorushizi/dependabot/npm_and_yarn/stylelint-config-standard-37.0.0
build(deps-dev): bump stylelint-config-standard from 36.0.0 to 37.0.0
2025-02-10 17:57:47 +08:00
曹儒士子 d4fe4c0aab Merge branch 'master' into dependabot/npm_and_yarn/stylelint-config-standard-37.0.0 2025-02-10 17:57:38 +08:00
曹儒士子 dd862e90bb Merge pull request #385 from caorushizi/dependabot/npm_and_yarn/eslint-plugin-react-hooks-5.1.0
build(deps-dev): bump eslint-plugin-react-hooks from 4.6.2 to 5.1.0
2025-02-10 17:56:49 +08:00
曹儒士子 33f02b5719 Merge branch 'master' into dependabot/npm_and_yarn/eslint-plugin-react-hooks-5.1.0 2025-02-10 17:56:21 +08:00
曹儒士子 ed8ea0861e Merge pull request #411 from caorushizi/dependabot/npm_and_yarn/vite-6.1.0
build(deps-dev): bump vite from 5.2.11 to 6.1.0
2025-02-10 17:55:50 +08:00
曹儒士子 1b445927dd Merge branch 'master' into dependabot/npm_and_yarn/vite-6.1.0 2025-02-10 17:55:33 +08:00
caorushizi b65e0f8bd6 Merge branch 'master' of https://github.com/caorushizi/mediago 2025-02-10 17:54:55 +08:00
caorushizi 9d30a04cdf ci: 🎡 disable automatic triggering 2025-02-10 17:53:59 +08:00
曹儒士子 134237fc5e Merge branch 'master' into dependabot/npm_and_yarn/vite-6.1.0 2025-02-10 17:49:19 +08:00
曹儒士子 1a933fc3a2 Merge pull request #412 from caorushizi/dev_videoName
fix: 🐛  change the name of the detected video
2025-02-10 17:47:48 +08:00
dependabot[bot] 63087901b9 build(deps-dev): bump vite from 5.2.11 to 6.1.0
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.2.11 to 6.1.0.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.1.0/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 04:50:17 +00:00
caorushizi bd56f2aa04 fix: 🐛 change the name of the detected video 2025-01-16 21:26:30 +08:00
曹儒士子 de86c861d8 Merge pull request #401 from caorushizi/v3.0.1
feat:   v3.0.1
2025-01-14 16:22:28 +08:00
caorushizi af3b8a2a6b feat: v3.0.1 2025-01-14 15:34:57 +08:00
dependabot[bot] 6b40256ee3 build(deps-dev): bump stylelint-config-standard from 36.0.0 to 37.0.0
Bumps [stylelint-config-standard](https://github.com/stylelint/stylelint-config-standard) from 36.0.0 to 37.0.0.
- [Release notes](https://github.com/stylelint/stylelint-config-standard/releases)
- [Changelog](https://github.com/stylelint/stylelint-config-standard/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint-config-standard/compare/36.0.0...37.0.0)

---
updated-dependencies:
- dependency-name: stylelint-config-standard
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 04:17:05 +00:00
dependabot[bot] d8f3f083c7 build(deps-dev): bump eslint-plugin-react-hooks from 4.6.2 to 5.1.0
Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 4.6.2 to 5.1.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-hooks
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-02 20:27:50 +00:00
曹儒士子 47d3d786ca Merge pull request #396 from caorushizi/dev/perfermance
Dev/perfermance
2025-01-03 04:25:10 +08:00
caorushizi 1c755b4bd5 feat: video sniffing page added one-click cleanup and one-click download 2025-01-03 04:19:31 +08:00
caorushizi 0cece2b194 perf: 🚀 use memoized function 2025-01-03 03:37:35 +08:00
caorushizi 840fc610f0 style: 💄 optimized the file reference path problem 2025-01-03 03:06:33 +08:00
caorushizi 6e55fe9f34 feat: some performance issues were optimized 2025-01-03 03:01:53 +08:00
dependabot[bot] c8c7351f9e build(deps): bump react-dom and @types/react-dom
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom). These dependencies needed to be updated together.

Updates `react-dom` from 18.3.1 to 19.0.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.0.0/packages/react-dom)

Updates `@types/react-dom` from 18.3.0 to 19.0.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-01 23:05:28 +00:00
dependabot[bot] 9bf2aa2c48 build(deps): bump socket.io-client from 4.8.0 to 4.8.1
Bumps [socket.io-client](https://github.com/socketio/socket.io) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-client@4.8.0...socket.io-client@4.8.1)

---
updated-dependencies:
- dependency-name: socket.io-client
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-01 23:05:26 +00:00
曹儒士子 1620b8e4c4 Merge pull request #395 from caorushizi/docs/packages
docs: 📝  simplify download buttons and supplement documentation
2025-01-02 07:03:12 +08:00
caorushizi f798bd3d8b feat: remove redux/toolkit 2025-01-02 06:54:49 +08:00
caorushizi 65850f158c docs: 📝 simplify download buttons and supplement documentation 2025-01-02 05:05:51 +08:00
曹儒士子 8f5991e65f Merge pull request #394 from caorushizi/feat/package-version
fix: 🐛  modify home page copy
2025-01-02 04:10:47 +08:00
caorushizi 5492e1df35 fix: 🐛 modify home page copy 2025-01-02 04:08:46 +08:00
曹儒士子 93fbd8768b Merge pull request #393 from caorushizi/feat/package-version
fix: 🐛  urlschema add headers params
2025-01-02 02:48:39 +08:00
caorushizi 246f079905 fix: 🐛 urlschema add headers params 2025-01-02 02:47:20 +08:00
曹儒士子 b25285fcc2 Merge pull request #392 from caorushizi/feat/package-version
feat:   use the silent parameter for immediate download
2025-01-02 02:33:36 +08:00
caorushizi 7a2b31d328 feat: use the silent parameter for immediate download 2025-01-02 02:32:47 +08:00
曹儒士子 a4ac847f0d Merge pull request #391 from caorushizi/feat/package-version
fix: 🐛  pass the request header when downloading
2025-01-02 01:19:50 +08:00
caorushizi 6b632b5502 fix: 🐛 pass the request header when downloading 2025-01-02 01:18:06 +08:00
dependabot[bot] 560a77ca52 build(deps): bump i18next-browser-languagedetector from 8.0.0 to 8.0.2
Bumps [i18next-browser-languagedetector](https://github.com/i18next/i18next-browser-languageDetector) from 8.0.0 to 8.0.2.
- [Changelog](https://github.com/i18next/i18next-browser-languageDetector/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-browser-languageDetector/compare/v8.0.0...v8.0.2)

---
updated-dependencies:
- dependency-name: i18next-browser-languagedetector
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-01 16:40:19 +00:00
曹儒士子 86e1345684 Merge pull request #389 from caorushizi/feat/package-version
feat:   update adblocker
2025-01-02 00:38:02 +08:00
caorushizi b91de0e442 feat: update adblocker
feat #388
2025-01-02 00:29:35 +08:00
曹儒士子 62e47aac4c Merge pull request #387 from caorushizi/fix/bug
Fix some bug
2025-01-01 18:27:28 +08:00
caorushizi 43fb92ac82 fix: 🐛 fixed issues with YouTube login and cloudflare verification 2025-01-01 13:38:56 +08:00
caorushizi de5ecb570f fix: 🐛 fixed the createDate type error 2025-01-01 13:34:51 +08:00
caorushizi 014c851a03 fix: 🐛 fixed last character counld not be deleted 2025-01-01 13:32:37 +08:00
caorushizi eae07b1d3b fix: 🐛 fix Bilibili video file name 2025-01-01 13:30:06 +08:00
caorushizi df06736722 fix: 🐛 windows script 2024-12-19 20:39:31 +08:00
曹儒士子 25b90a0993 Merge pull request #378 from caorushizi/dev/docs
feat:   update readme
2024-12-19 20:11:45 +08:00
caorushizi ad1b58bf53 feat: update readme 2024-12-19 20:10:49 +08:00
曹儒士子 259118e48b Merge pull request #377 from caorushizi/dev/docs
feat:   update readme
2024-12-19 20:08:46 +08:00
caorushizi 145ac6bb12 feat: update readme 2024-12-19 20:08:01 +08:00
曹儒士子 7b581ff917 Merge pull request #376 from caorushizi/dev/docs
Dev/docs
2024-12-19 17:16:27 +08:00
caorushizi 62423f8156 feat: update README.md 2024-12-19 17:15:10 +08:00
caorushizi 5fd539445e docs: 📝 use English annotations 2024-12-19 16:54:28 +08:00
曹儒士子 7c756cccef feat: docker platform (#371)
* feat:   docker platform

* feat:   docker arch
2024-12-15 10:56:35 +08:00
曹儒士子 786826b434 Merge pull request #370 from woniuxingdong/master
feat: display created date for items in download list
2024-12-14 11:56:52 +08:00
spencer 56f41999b5 feat: display created date for items in download list 2024-12-13 17:42:58 +08:00
曹儒士子 7423cf89ab Merge pull request #369 from caorushizi/dev-bt
docs: update README.md
2024-12-13 16:03:32 +08:00
caorushizi 8c6ee20b30 docs: update README.md 2024-12-13 16:02:03 +08:00
曹儒士子 17ffa1acc0 Merge pull request #368 from woniuxingdong/master 2024-12-12 20:43:13 +08:00
spencer cec95c9f0a fix: add one setting item to control if the browser need to play audio 2024-12-12 17:33:49 +08:00
spencer eaf3d46e24 fix: defaultly to play audio 2024-12-10 16:48:47 +08:00
曹儒士子 c30badabbf Merge pull request #365 from caorushizi/feat/remove-docs
docs: 📝  remove docs
2024-12-09 12:56:18 +08:00
caorushizi 40cdecc820 docs: 📝 remove docs 2024-12-09 12:55:18 +08:00
caorushizi 3c1088b6d6 feat: website SEO 2024-11-28 17:18:20 +08:00
士子☀️ ac9a4c8f48 Update README.md 2024-10-17 11:03:32 +08:00
caorushizi b79ab2ee3f feat: release v3.0 2024-10-08 00:32:48 +08:00
caorushizi ceef11bca6 feat: v3.0.0 2024-10-08 00:23:01 +08:00
士子☀️ 5ce80eddb6 Merge pull request #323 from caorushizi/v3.0.0
v3.0.0
2024-10-07 22:37:04 +08:00
caorushizi 8bc3e229d7 v3.0.0 2024-10-07 22:34:46 +08:00
士子☀️ f939ce138c Merge pull request #322 from caorushizi/dev-docker-1
feat:   release scripts
2024-10-07 17:34:35 +08:00
caorushizi ad89281927 feat: release scripts 2024-10-07 17:33:53 +08:00
士子☀️ c9ae9e6b32 Fix mac build (#319)
* fix: 🐛  Mac build
#230 修复 intel 无法打开的问题
2024-10-07 14:53:08 +08:00
士子☀️ ce7dd57d36 Merge pull request #314 from caorushizi/fix-mac-build
Fix mac build
2024-10-07 00:59:36 +08:00
caorushizi 43ed1f8e3f fix: 🐛 mac build 2024-10-07 00:52:52 +08:00
caorushizi 9d1e796a20 fix: 🐛 mac build 2024-10-07 00:48:06 +08:00
士子☀️ 80bfe5f979 Merge pull request #313 from caorushizi/v3.0.0-beta.5
feat:   v3.0.0-beta.5
2024-10-06 17:12:57 +08:00
caorushizi 13ee214026 feat: v3.0.0-beta.5 2024-10-06 17:09:59 +08:00
士子☀️ 08e38d752a Merge pull request #312 from caorushizi/v3.0.0-beta.3
feat:   v3.0.0-beta.3
2024-10-06 16:38:44 +08:00
caorushizi aef4d4ffee feat: v3.0.0-beta.3 2024-10-06 16:37:28 +08:00
士子☀️ a1b2c70fdd Merge pull request #311 from caorushizi/v3.0.0-beta.3
feat:   v3.0.0-beta.3
2024-10-06 16:32:47 +08:00
caorushizi c9b4525baf feat: v3.0.0-beta.3 2024-10-06 16:30:59 +08:00
caorushizi 0b0666912e feat: supports urlscheme
#203
2024-10-04 23:47:29 +08:00
士子☀️ 9dbb5681f7 Merge pull request #310 from caorushizi/fix-merge-video
fix: 🐛  merge video
2024-10-04 16:11:34 +08:00
caorushizi eaa7bdd736 fix: 🐛 merge video 2024-10-04 16:10:28 +08:00
士子☀️ 0a65bbae5d Merge pull request #307 from caorushizi/dev-gulp
Dev gulp
2024-10-04 14:43:28 +08:00
caorushizi 82cfc1cabb feat: live streams are automatically merged 2024-10-04 14:42:11 +08:00
caorushizi f4402ac57b feat: add download folder 2024-10-04 14:26:26 +08:00
士子☀️ f6514f85dc Merge pull request #306 from caorushizi/dev-gulp
feat:   optimize web Some interaction & optimize engineering logic
2024-10-04 00:01:57 +08:00
caorushizi 1cf4c0ce39 feat: optimize web Some interaction & optimize engineering logic 2024-10-03 23:59:26 +08:00
士子☀️ 3ffe7a63e9 Merge pull request #305 from david88558855/master
Update README.md
2024-10-03 22:24:13 +08:00
david88558855 a532d9d209 Update README.md 2024-10-03 22:21:02 +08:00
士子☀️ 9c1f2f6662 Merge pull request #304 from caorushizi/dev-docker
fix: 🐛  compressed image volume
2024-10-03 14:45:20 +08:00
caorushizi 0b9ef9ff4c fix: 🐛 compressed image volume 2024-10-03 11:29:28 +08:00
caorushizi 35f25abd8a fix: 🐛 fragments are not deleted after downloading 2024-10-02 23:22:40 +08:00
士子☀️ 70bed8c006 Merge pull request #303 from caorushizi/package-version
fix: 🐛  lock version
2024-10-02 17:12:21 +08:00
caorushizi dcfad6a3c0 fix: 🐛 lock version 2024-10-02 17:01:24 +08:00
士子☀️ e7d1540b72 Merge pull request #302 from caorushizi/docker
feat:   support docker
2024-10-02 16:27:14 +08:00
caorushizi 4bc09d74ff feat: support docker 2024-10-02 16:23:47 +08:00
士子☀️ 7ab45498e6 Merge pull request #301 from caorushizi/fix-charset
Fix charset
2024-10-01 22:17:22 +08:00
caorushizi da7ffd9c12 feat: docker file 2024-10-01 22:15:51 +08:00
caorushizi b5d7ec111d feat: remote docker image 2024-10-01 21:52:07 +08:00
caorushizi 9077eb836c fix: 🐛 database charset 2024-10-01 20:32:08 +08:00
士子☀️ 6f3c2666ca Merge pull request #300 from caorushizi/dev-web
feat:   dev web
2024-10-01 01:28:10 +08:00
caorushizi eaa3170f01 feat: support web 2024-10-01 01:26:55 +08:00
caorushizi 7e1c425676 feat: dev web 2024-09-30 23:02:25 +08:00
caorushizi 7df980f881 feat: dev web 2024-09-30 18:28:34 +08:00
士子☀️ 7f0259e9cc Merge pull request #294 from caorushizi/dev-web
feat:   web
2024-09-30 13:14:24 +08:00
caorushizi c7b2f99c43 feat: backend log 2024-09-30 13:12:57 +08:00
caorushizi 4534238216 feat: nodejs 2024-09-30 03:04:57 +08:00
caorushizi 2085ef3063 feat: web 2024-09-29 02:39:49 +08:00
士子☀️ 7d3d69eae5 Merge pull request #293 from caorushizi/fix-list-style
fix: 🐛  fix list style
2024-09-28 15:06:49 +08:00
caorushizi 5c129e050b feat: nodejs version 2024-09-28 15:01:59 +08:00
caorushizi 73163de408 fix: 🐛 fix list style 2024-09-27 02:46:36 +08:00
士子☀️ f18bdc4375 Merge pull request #292 from caorushizi/fix-error
fix: 🐛  error message style in dark mode
2024-09-26 23:24:55 +08:00
caorushizi 2d9a226378 fix: 🐛 error message style in dark mode 2024-09-26 23:23:42 +08:00
士子☀️ 6cd4094159 Merge pull request #291 from caorushizi/dev-icon
fix: 🐛  optimize some styles
2024-09-26 23:06:30 +08:00
caorushizi d4c43ece23 fix: 🐛 optimize some styles 2024-09-26 22:58:35 +08:00
士子☀️ 9ac8d5d2bf Merge pull request #290 from caorushizi/dev-icon
feat:   optimized app ICONS
2024-09-26 21:52:22 +08:00
caorushizi 049ef746f4 feat: optimized app ICONS 2024-09-26 21:51:09 +08:00
士子☀️ 83d26211a0 Merge pull request #289 from caorushizi/v3.0.0-beta.2
feat:   v3.0.0-beta.2
2024-09-26 19:42:49 +08:00
caorushizi 680641707e feat: v3.0.0-beta.2 2024-09-26 19:40:58 +08:00
士子☀️ 27d4e092cf Merge pull request #288 from caorushizi/feature-export
feat:   added video link export & new download style optimization
2024-09-26 19:38:29 +08:00
caorushizi 5b477b9e9e feat: added video link export & new download style optimization 2024-09-26 19:36:59 +08:00
士子☀️ eb332b70e0 Merge pull request #287 from caorushizi/feature
feat:   video sniffing logic optimization
2024-09-26 17:59:52 +08:00
caorushizi 824df9cae2 feat: video sniffing logic optimization 2024-09-26 17:54:37 +08:00
士子☀️ 1fc73f324c Merge pull request #286 from caorushizi/feature
Feature
2024-09-26 16:47:15 +08:00
caorushizi 8dcdb1f8a3 fix: 🐛 automatically fill the video name
#127
2024-09-26 16:46:04 +08:00
caorushizi 4fe1d1ea66 feat: upgrade download program 2024-09-26 16:24:27 +08:00
士子☀️ 93b6f8c7b6 Merge pull request #285 from caorushizi/feature
Feature
2024-09-26 15:37:10 +08:00
caorushizi a33e7c3903 fix: 🐛 fixed an issue with ICONS adding favorites 2024-09-26 15:34:32 +08:00
caorushizi 4ea0180781 feat: display error message when download fails 2024-09-26 12:37:28 +08:00
caorushizi 1d4850d7fb feat: tdapp 2024-09-26 11:58:59 +08:00
士子☀️ cf77e6633e Merge pull request #284 from caorushizi/feature
feat:   new icon
2024-09-26 03:43:43 +08:00
caorushizi c692fdf6c3 feat: new icon 2024-09-26 03:41:24 +08:00
士子☀️ 1901b177cc Merge pull request #283 from caorushizi/feature
feat:   添加关闭窗口选项
2024-09-26 02:49:49 +08:00
caorushizi ce60d77c8d feat: 添加关闭窗口选项 2024-09-26 02:47:46 +08:00
士子☀️ fbe1d43488 Merge pull request #282 from caorushizi/feature
Feature
2024-09-26 02:21:42 +08:00
caorushizi ee8e75513d feat: 版本更新检查 2024-09-26 02:19:12 +08:00
caorushizi f094fd1e7f feat: main process adds multiple languages 2024-09-25 21:34:59 +08:00
士子☀️ 14c30e0e58 Merge pull request #281 from caorushizi/feature-batch
fix: 🐛  批量下载功能优化
2024-09-25 14:01:05 +08:00
caorushizi 872f1b9284 fix: 🐛 批量下载功能优化
#244, #198
2024-09-25 13:59:22 +08:00
士子☀️ 5680512573 Merge pull request #279 from caorushizi/v3.0.0-beta.1
feat:   v3.0.0-beta.1
2024-09-25 03:33:48 +08:00
caorushizi 2f6a43d98a feat: v3.0.0-beta.1 2024-09-25 03:32:14 +08:00
士子☀️ 1142c2c501 Merge pull request #278 from caorushizi/fix-issues
fix: 🐛  便携版上传
2024-09-25 03:20:01 +08:00
caorushizi c1502c0230 fix: 🐛 便携版上传 2024-09-25 03:04:59 +08:00
士子☀️ 55cf680b0c Merge pull request #276 from caorushizi/fix-issues
feat(download):   新建下载时默认带入上次表单信息
2024-09-25 02:54:25 +08:00
caorushizi 6c0d0cdf74 feat: 新增便携版 2024-09-25 02:52:40 +08:00
caorushizi 32ad030838 fix: 🐛 阻止 deeplink 打开 2024-09-25 01:55:21 +08:00
caorushizi 9811c18407 fix: 🐛 优化视频识别逻辑 & 添加完成列表的右键菜单 2024-09-25 01:44:30 +08:00
caorushizi a9c6e01294 fix: 🐛 修复批量下载 & icon 样式 2024-09-25 00:48:37 +08:00
caorushizi b904d6aaec fix: 🐛 pC 端的视频无法播放 2024-09-24 22:36:03 +08:00
caorushizi 78958d925a fix: 🐛 解决列表页面在操作时跳动的问题 2024-09-24 21:55:11 +08:00
caorushizi afba340028 fix: 🐛 新建收藏之后没有关闭弹窗 2024-09-24 21:03:07 +08:00
caorushizi ec4b24149f fix: 🐛 修复收藏页面中无法通过右键删除的问题 2024-09-24 20:52:02 +08:00
caorushizi 8e1bd8596f feat: 增加导入导出网址
#205
2024-09-24 20:33:03 +08:00
caorushizi f8785f8db8 fix: 🐛 收藏列表样式优化 2024-09-24 14:49:10 +08:00
caorushizi c508c1dd1a fix: 🐛 构建产物添加平台的名称 2024-09-24 13:28:11 +08:00
caorushizi c78b593afa feat: 将首页的【打开浏览器】按钮换成【素材提取】
#238
2024-09-24 13:20:10 +08:00
caorushizi 1b906804de feat(download): 新建下载时默认带入上次表单信息
#273 #273
2024-09-24 13:02:27 +08:00
士子☀️ 62561e9826 Merge pull request #265 from caorushizi/release-v3.0.0-beta.0
feat:   release 3.0.0-beta.0
2024-09-01 16:32:13 +08:00
caorushizi cbb9798174 feat: release 3.0.0-beta.0 2024-09-01 16:30:44 +08:00
caorushizi 2de6b6a1cc fix: 🐛 修复一些 bug 2024-09-01 16:28:00 +08:00
士子☀️ 5893d1492e Merge pull request #264 from caorushizi/fix/download-list-ui
fix: 🐛  fix: tag ui scaling bug
2024-09-01 01:52:19 +08:00
Charlie 5906f29180 fix: 🐛 fix: tag ui scaling bug 2024-09-01 01:32:00 +08:00
caorushizi 9b838e496e feat: fix macos 2024-08-27 02:12:24 +08:00
士子☀️ 106a1fa9eb Merge pull request #261 from caorushizi/dev-backend
Dev backend
2024-08-27 01:14:29 +08:00
caorushizi ecfb2371f4 feat: macos 2024-08-27 01:11:00 +08:00
caorushizi fe3ae95a22 feat: web 初步 2024-08-25 17:35:13 +08:00
士子☀️ 4ae352d5d2 Merge pull request #255 from caorushizi/dev-backend
feat:   add backend
2024-08-22 02:52:55 +08:00
caorushizi 67d56fc5ab feat: 完成 backend 基础工程 & 新增 docker 配置 2024-08-22 02:50:58 +08:00
士子☀️ 039f9eb092 Merge pull request #254 from caorushizi/dev-ui
feat:   local ip
2024-08-14 13:40:48 +08:00
caorushizi df67e52e47 feat: add backend 2024-08-14 13:38:31 +08:00
caorushizi e3f7c48f80 feat: local ip 2024-08-11 21:21:06 +08:00
士子☀️ 57b67ea55c Merge pull request #253 from caorushizi/dev-ui
feat:   add mobile player
2024-08-11 20:47:27 +08:00
caorushizi 21c3eb475f feat: add mobile player 2024-08-11 20:45:59 +08:00
士子☀️ 805f9435f3 Merge pull request #252 from caorushizi/dev-ui
feat:   new ui
2024-08-11 18:16:15 +08:00
士子☀️ 213261fbbb Merge pull request #250 from caorushizi/dev-ui
Dev UI
2024-08-11 18:10:25 +08:00
caorushizi ceda93a8cf feat: new ui 2024-08-11 18:06:24 +08:00
caorushizi 39ec4f863d feat: new ui 2024-08-11 18:02:33 +08:00
caorushizi 4d9188221f feat: new ui 2024-08-11 01:23:16 +08:00
caorushizi 30c8085964 feat: new ui 2024-08-11 01:14:24 +08:00
士子☀️ 97206279d8 Merge pull request #249 from ruixingshi/srx/add-version-info 2024-08-10 20:18:46 +08:00
shiruixing 8d3c8f4ad1 docs: change text 2024-08-10 20:15:27 +08:00
士子☀️ bf4075ba1c Merge pull request #248 from ruixingshi/srx/add-version-info 2024-08-10 18:23:43 +08:00
shiruixing 5f5892242b docs: add update info 2024-08-10 18:17:58 +08:00
fenglei 29602d9fa2 docs: update icon button (#243) 2024-08-07 18:37:23 +08:00
Hanzc f2f1f963d4 docs: 创建 axios 实例 (#242) 2024-08-07 18:29:00 +08:00
士子☀️ 56219c8fe4 V3.0.0 (#235)
* feat:   v3.0.0

* feat:   v3.0.0
2024-07-28 23:18:22 +08:00
士子☀️ ba211c1360 feat: v3.0.0 (#234)
* feat:   v3.0.0

* feat:   v3.0.0
2024-07-25 00:00:33 +08:00
caorushizi 2870a42cd4 feat: new ui 2024-07-23 22:09:30 +08:00
caorushizi f8a2188760 feat(ui): new ui 2024-07-23 22:09:30 +08:00
caorushizi 5c4020a52f fix: 🐛 remove @tailwindcss/line-clamp 2024-07-23 22:09:30 +08:00
caorushizi 4c060366b8 fix: 🐛 修复 @cliqz/adblocker 版本问题 2024-07-23 22:09:30 +08:00
caorushizi af219bde11 feat(ui): new ui 2024-07-23 22:09:30 +08:00
caorushizi caa279ce36 feat(ui): new ui 2024-07-23 22:09:30 +08:00
caorushizi 4651229a0d feat: update dependencies 2024-07-23 22:09:30 +08:00
caorushizi 6c8916a9e6 feat: new ui 2024-07-23 22:09:30 +08:00
caorushizi 8d831e2c9d feat(ui): new ui 2024-07-23 22:09:30 +08:00
caorushizi 1f3b4f5624 feat: new ui 2024-07-23 22:09:30 +08:00
caorushizi 04c8afe616 feat: ui 2024-07-23 22:09:30 +08:00
caorushizi e53aed0910 feat(ui): style revisions 2024-07-23 22:09:30 +08:00
caorushizi 520b63a8de feat: add mobile project 2024-07-23 22:09:30 +08:00
caorushizi 0e4aa730eb feat: add video server 2024-07-23 22:09:30 +08:00
caorushizi 07c7ddccad feat: 新 UI 重构 2024-07-23 22:09:30 +08:00
caorushizi 5ff7252ee9 feat: add tailwindcss 2024-07-23 22:09:30 +08:00
士子☀️ 5461e74539 Merge pull request #216 from caorushizi/release-v2.2.3
feat:   v2.2.3
2024-07-06 17:42:02 +08:00
caorushizi 417484cb6d feat: v2.2.3 2024-07-06 17:38:30 +08:00
士子☀️ 9aaffd3550 Merge pull request #214 from caorushizi/dev-docs
docs: 📝  更新文档
2024-07-03 15:40:36 +08:00
caorushizi 94759d48c7 docs: 📝 更新文档 2024-07-03 15:37:16 +08:00
士子☀️ c0e343b696 Merge pull request #204 from caorushizi/fix-search
fix: 🐛  修复新版本在更换安装目录后下载失败的问题
2024-06-30 19:01:26 +08:00
caorushizi f18ac7c444 fix: 🐛 修复新版本在更换安装目录后下载失败的问题
下载中的视频无法停止 #202
2024-06-30 18:58:58 +08:00
士子☀️ f637610734 Merge pull request #196 from caorushizi/fix-search
fix: 🐛  修复搜索时跳转的链接
2024-06-24 23:43:47 +08:00
caorushizi 5e1046dd5b fix: 🐛 修复搜索时跳转的链接 2024-06-23 11:41:33 +08:00
士子☀️ f25584cb2f Merge pull request #195 from caorushizi/fix-videoname
fix: 🐛  修复获取视频名称的问题
2024-06-23 00:13:13 +08:00
caorushizi 03999585d0 fix: 🐛 修复获取视频名称的问题 2024-06-23 00:12:06 +08:00
士子☀️ 772c2b0300 Merge pull request #194 from caorushizi/dev-webview
Dev webview
2024-06-22 22:59:28 +08:00
caorushizi 3622e67a56 fix: 🐛 修复浏览器背景颜色 2024-06-22 22:58:25 +08:00
caorushizi c51630cdd6 fix: 🐛 修复切换隐私模式时 Proxy 和嗅探问题 2024-06-22 22:14:49 +08:00
士子☀️ dab1c55a93 Merge pull request #193 from caorushizi/dev-webview
feat(settings):   新增无痕浏览模式
2024-06-22 21:48:11 +08:00
caorushizi 7b966bc988 feat(settings): 新增无痕浏览模式
设置页面新增无痕浏览模式

新增无痕浏览模式 #52
2024-06-22 21:46:27 +08:00
士子☀️ e5e46bee38 Merge pull request #192 from caorushizi/dev-webview
feat(settings):   添加自动更新选项
2024-06-22 15:55:09 +08:00
caorushizi 3365d96a03 feat(settings): 添加自动更新选项
设置页面添加自动更新选项,下次启动时生效

auto upgrade #185
2024-06-22 15:53:42 +08:00
士子☀️ fb223cd493 Merge pull request #191 from caorushizi/dev-webview
feat:   添加下载代理开关 & 添加清空缓存
2024-06-22 15:36:18 +08:00
caorushizi 60d49c4773 feat: 添加下载代理开关 & 添加清空缓存 2024-06-22 15:35:08 +08:00
士子☀️ e7aa4ae147 Merge pull request #190 from caorushizi/dev-webview
build: 🛠  portable
2024-06-22 12:42:50 +08:00
caorushizi c45be41ddc build: 🛠 portable 2024-06-22 12:41:45 +08:00
士子☀️ 85b37facee Merge pull request #189 from caorushizi/dev-webview
Dev webview
2024-06-22 12:27:58 +08:00
caorushizi 90567009ef Merge branch 'dev-webview' of https://github.com/caorushizi/m3u8-downloader into dev-webview 2024-06-22 12:26:36 +08:00
caorushizi 899afc156e build: 🛠 upload artifact 2024-06-22 12:25:51 +08:00
caorushizi 9e33b7e488 build: 🛠 upload artifact 2024-06-22 12:24:24 +08:00
士子☀️ e7ba60a51f Merge pull request #188 from caorushizi/dev-webview
build: 🛠  portable
2024-06-22 12:11:55 +08:00
caorushizi 2a3ad325c2 build: 🛠 portable 2024-06-22 12:11:19 +08:00
caorushizi 7575b80d31 build: 🛠 portable 2024-06-22 12:08:33 +08:00
士子☀️ 8bb9d119b5 Merge pull request #187 from caorushizi/dev-webview
Dev webview
2024-06-22 11:54:18 +08:00
caorushizi 5da3f2b1c7 build: 🛠 优化构建 2024-06-22 11:50:40 +08:00
caorushizi da6cbcfb75 feat: 修复 webview 中加载的问题 2024-06-22 10:44:34 +08:00
caorushizi 3d7aff41ee feat: 优化内置浏览器逻辑 2024-06-22 03:42:15 +08:00
士子☀️ 2cc49e8679 Merge pull request #186 from caorushizi/dev-ap
feat:   ap
2024-06-21 23:44:42 +08:00
caorushizi 0e961c0647 feat: use td app 2024-06-21 23:39:45 +08:00
caorushizi 4d5757a6ce feat: ap 2024-06-21 00:04:21 +08:00
782 changed files with 87061 additions and 15140 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,127 @@
# React Best Practices
A structured repository for creating and maintaining React Best Practices optimized for agents and LLMs.
## Structure
- `rules/` - Individual rule files (one per rule)
- `_sections.md` - Section metadata (titles, impacts, descriptions)
- `_template.md` - Template for creating new rules
- `area-description.md` - Individual rule files
- `src/` - Build scripts and utilities
- `metadata.json` - Document metadata (version, organization, abstract)
- **`AGENTS.md`** - Compiled output (generated)
- **`test-cases.json`** - Test cases for LLM evaluation (generated)
## Getting Started
1. Install dependencies:
```bash
pnpm install
```
2. Build AGENTS.md from rules:
```bash
pnpm build
```
3. Validate rule files:
```bash
pnpm validate
```
4. Extract test cases:
```bash
pnpm extract-tests
```
## Creating a New Rule
1. Copy `rules/_template.md` to `rules/area-description.md`
2. Choose the appropriate area prefix:
- `async-` for Eliminating Waterfalls (Section 1)
- `bundle-` for Bundle Size Optimization (Section 2)
- `server-` for Server-Side Performance (Section 3)
- `client-` for Client-Side Data Fetching (Section 4)
- `rerender-` for Re-render Optimization (Section 5)
- `rendering-` for Rendering Performance (Section 6)
- `js-` for JavaScript Performance (Section 7)
- `advanced-` for Advanced Patterns (Section 8)
3. Fill in the frontmatter and content
4. Ensure you have clear examples with explanations
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
## Rule File Structure
Each rule file should follow this structure:
````markdown
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description
tags: tag1, tag2, tag3
---
## Rule Title Here
Brief explanation of the rule and why it matters.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example
```
````
**Correct (description of what's right):**
```typescript
// Good code example
```
Optional explanatory text after examples.
Reference: [Link](https://example.com)
## File Naming Convention
- Files starting with `_` are special (excluded from build)
- Rule files: `area-description.md` (e.g., `async-parallel.md`)
- Section is automatically inferred from filename prefix
- Rules are sorted alphabetically by title within each section
- IDs (e.g., 1.1, 1.2) are auto-generated during build
## Impact Levels
- `CRITICAL` - Highest priority, major performance gains
- `HIGH` - Significant performance improvements
- `MEDIUM-HIGH` - Moderate-high gains
- `MEDIUM` - Moderate performance improvements
- `LOW-MEDIUM` - Low-medium gains
- `LOW` - Incremental improvements
## Scripts
- `pnpm build` - Compile rules into AGENTS.md
- `pnpm validate` - Validate all rule files
- `pnpm extract-tests` - Extract test cases for LLM evaluation
- `pnpm dev` - Build and validate
## Contributing
When adding or modifying rules:
1. Use the correct filename prefix for your section
2. Follow the `_template.md` structure
3. Include clear bad/good examples with explanations
4. Add appropriate tags
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
6. Rules are automatically sorted by title - no need to manage numbers!
## Acknowledgments
Originally created by [@shuding](https://x.com/shuding) at [Vercel](https://vercel.com).
@@ -0,0 +1,148 @@
---
name: vercel-react-best-practices
description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
license: MIT
metadata:
author: vercel
version: "1.0.0"
---
# Vercel React Best Practices
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 67 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
## When to Apply
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
- Reviewing code for performance issues
- Refactoring existing React/Next.js code
- Optimizing bundle size or load times
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
| -------- | ------------------------- | ----------- | ------------ |
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
| 3 | Server-Side Performance | HIGH | `server-` |
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
| 6 | Rendering Performance | MEDIUM | `rendering-` |
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
| 8 | Advanced Patterns | LOW | `advanced-` |
## Quick Reference
### 1. Eliminating Waterfalls (CRITICAL)
- `async-cheap-condition-before-await` - Check cheap sync conditions before awaiting flags or remote values
- `async-defer-await` - Move await into branches where actually used
- `async-parallel` - Use Promise.all() for independent operations
- `async-dependencies` - Use better-all for partial dependencies
- `async-api-routes` - Start promises early, await late in API routes
- `async-suspense-boundaries` - Use Suspense to stream content
### 2. Bundle Size Optimization (CRITICAL)
- `bundle-barrel-imports` - Import directly, avoid barrel files
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
- `bundle-defer-third-party` - Load analytics/logging after hydration
- `bundle-conditional` - Load modules only when feature is activated
- `bundle-preload` - Preload on hover/focus for perceived speed
### 3. Server-Side Performance (HIGH)
- `server-auth-actions` - Authenticate server actions like API routes
- `server-cache-react` - Use React.cache() for per-request deduplication
- `server-cache-lru` - Use LRU cache for cross-request caching
- `server-dedup-props` - Avoid duplicate serialization in RSC props
- `server-hoist-static-io` - Hoist static I/O (fonts, logos) to module level
- `server-serialization` - Minimize data passed to client components
- `server-parallel-fetching` - Restructure components to parallelize fetches
- `server-parallel-nested-fetching` - Chain nested fetches per item in Promise.all
- `server-after-nonblocking` - Use after() for non-blocking operations
### 4. Client-Side Data Fetching (MEDIUM-HIGH)
- `client-swr-dedup` - Use SWR for automatic request deduplication
- `client-event-listeners` - Deduplicate global event listeners
- `client-passive-event-listeners` - Use passive listeners for scroll
- `client-localstorage-schema` - Version and minimize localStorage data
### 5. Re-render Optimization (MEDIUM)
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
- `rerender-memo` - Extract expensive work into memoized components
- `rerender-memo-with-default-value` - Hoist default non-primitive props
- `rerender-dependencies` - Use primitive dependencies in effects
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
- `rerender-derived-state-no-effect` - Derive state during render, not effects
- `rerender-functional-setstate` - Use functional setState for stable callbacks
- `rerender-lazy-state-init` - Pass function to useState for expensive values
- `rerender-simple-expression-in-memo` - Avoid memo for simple primitives
- `rerender-split-combined-hooks` - Split hooks with independent dependencies
- `rerender-move-effect-to-event` - Put interaction logic in event handlers
- `rerender-transitions` - Use startTransition for non-urgent updates
- `rerender-use-deferred-value` - Defer expensive renders to keep input responsive
- `rerender-use-ref-transient-values` - Use refs for transient frequent values
- `rerender-no-inline-components` - Don't define components inside components
### 6. Rendering Performance (MEDIUM)
- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
- `rendering-content-visibility` - Use content-visibility for long lists
- `rendering-hoist-jsx` - Extract static JSX outside components
- `rendering-svg-precision` - Reduce SVG coordinate precision
- `rendering-hydration-no-flicker` - Use inline script for client-only data
- `rendering-hydration-suppress-warning` - Suppress expected mismatches
- `rendering-activity` - Use Activity component for show/hide
- `rendering-conditional-render` - Use ternary, not && for conditionals
- `rendering-usetransition-loading` - Prefer useTransition for loading state
- `rendering-resource-hints` - Use React DOM resource hints for preloading
- `rendering-script-defer-async` - Use defer or async on script tags
### 7. JavaScript Performance (LOW-MEDIUM)
- `js-batch-dom-css` - Group CSS changes via classes or cssText
- `js-index-maps` - Build Map for repeated lookups
- `js-cache-property-access` - Cache object properties in loops
- `js-cache-function-results` - Cache function results in module-level Map
- `js-cache-storage` - Cache localStorage/sessionStorage reads
- `js-combine-iterations` - Combine multiple filter/map into one loop
- `js-length-check-first` - Check array length before expensive comparison
- `js-early-exit` - Return early from functions
- `js-hoist-regexp` - Hoist RegExp creation outside loops
- `js-min-max-loop` - Use loop for min/max instead of sort
- `js-set-map-lookups` - Use Set/Map for O(1) lookups
- `js-tosorted-immutable` - Use toSorted() for immutability
- `js-flatmap-filter` - Use flatMap to map and filter in one pass
- `js-request-idle-callback` - Defer non-critical work to browser idle time
### 8. Advanced Patterns (LOW)
- `advanced-event-handler-refs` - Store event handlers in refs
- `advanced-init-once` - Initialize app once per app load
- `advanced-use-latest` - useLatest for stable callback refs
## How to Use
Read individual rule files for detailed explanations and code examples:
```
rules/async-parallel.md
rules/bundle-barrel-imports.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
- Additional context and references
## Full Compiled Document
For the complete guide with all rules expanded: `AGENTS.md`
@@ -0,0 +1,46 @@
# Sections
This file defines all sections, their ordering, impact levels, and descriptions.
The section ID (in parentheses) is the filename prefix used to group rules.
---
## 1. Eliminating Waterfalls (async)
**Impact:** CRITICAL
**Description:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
## 2. Bundle Size Optimization (bundle)
**Impact:** CRITICAL
**Description:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
## 3. Server-Side Performance (server)
**Impact:** HIGH
**Description:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
## 4. Client-Side Data Fetching (client)
**Impact:** MEDIUM-HIGH
**Description:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
## 5. Re-render Optimization (rerender)
**Impact:** MEDIUM
**Description:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
## 6. Rendering Performance (rendering)
**Impact:** MEDIUM
**Description:** Optimizing the rendering process reduces the work the browser needs to do.
## 7. JavaScript Performance (js)
**Impact:** LOW-MEDIUM
**Description:** Micro-optimizations for hot paths can add up to meaningful improvements.
## 8. Advanced Patterns (advanced)
**Impact:** LOW
**Description:** Advanced patterns for specific cases that require careful implementation.
@@ -0,0 +1,28 @@
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description of impact (e.g., "20-50% improvement")
tags: tag1, tag2
---
## Rule Title Here
**Impact: MEDIUM (optional impact description)**
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example here
const bad = example();
```
**Correct (description of what's right):**
```typescript
// Good code example here
const good = example();
```
Reference: [Link to documentation or resource](https://example.com)
@@ -0,0 +1,55 @@
---
title: Store Event Handlers in Refs
impact: LOW
impactDescription: stable subscriptions
tags: advanced, hooks, refs, event-handlers, optimization
---
## Store Event Handlers in Refs
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
**Incorrect (re-subscribes on every render):**
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
useEffect(() => {
window.addEventListener(event, handler);
return () => window.removeEventListener(event, handler);
}, [event, handler]);
}
```
**Correct (stable subscription):**
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
const handlerRef = useRef(handler);
useEffect(() => {
handlerRef.current = handler;
}, [handler]);
useEffect(() => {
const listener = (e) => handlerRef.current(e);
window.addEventListener(event, listener);
return () => window.removeEventListener(event, listener);
}, [event]);
}
```
**Alternative: use `useEffectEvent` if you're on latest React:**
```tsx
import { useEffectEvent } from "react";
function useWindowEvent(event: string, handler: (e) => void) {
const onEvent = useEffectEvent(handler);
useEffect(() => {
window.addEventListener(event, onEvent);
return () => window.removeEventListener(event, onEvent);
}, [event]);
}
```
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
@@ -0,0 +1,42 @@
---
title: Initialize App Once, Not Per Mount
impact: LOW-MEDIUM
impactDescription: avoids duplicate init in development
tags: initialization, useEffect, app-startup, side-effects
---
## Initialize App Once, Not Per Mount
Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
**Incorrect (runs twice in dev, re-runs on remount):**
```tsx
function Comp() {
useEffect(() => {
loadFromStorage();
checkAuthToken();
}, []);
// ...
}
```
**Correct (once per app load):**
```tsx
let didInit = false;
function Comp() {
useEffect(() => {
if (didInit) return;
didInit = true;
loadFromStorage();
checkAuthToken();
}, []);
// ...
}
```
Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
@@ -0,0 +1,39 @@
---
title: useEffectEvent for Stable Callback Refs
impact: LOW
impactDescription: prevents effect re-runs
tags: advanced, hooks, useEffectEvent, refs, optimization
---
## useEffectEvent for Stable Callback Refs
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
**Incorrect (effect re-runs on every callback change):**
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState("");
useEffect(() => {
const timeout = setTimeout(() => onSearch(query), 300);
return () => clearTimeout(timeout);
}, [query, onSearch]);
}
```
**Correct (using React's useEffectEvent):**
```tsx
import { useEffectEvent } from "react";
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState("");
const onSearchEvent = useEffectEvent(onSearch);
useEffect(() => {
const timeout = setTimeout(() => onSearchEvent(query), 300);
return () => clearTimeout(timeout);
}, [query]);
}
```
@@ -0,0 +1,38 @@
---
title: Prevent Waterfall Chains in API Routes
impact: CRITICAL
impactDescription: 2-10× improvement
tags: api-routes, server-actions, waterfalls, parallelization
---
## Prevent Waterfall Chains in API Routes
In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
**Incorrect (config waits for auth, data waits for both):**
```typescript
export async function GET(request: Request) {
const session = await auth();
const config = await fetchConfig();
const data = await fetchData(session.user.id);
return Response.json({ data, config });
}
```
**Correct (auth and config start immediately):**
```typescript
export async function GET(request: Request) {
const sessionPromise = auth();
const configPromise = fetchConfig();
const session = await sessionPromise;
const [config, data] = await Promise.all([
configPromise,
fetchData(session.user.id),
]);
return Response.json({ data, config });
}
```
For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
@@ -0,0 +1,37 @@
---
title: Check Cheap Conditions Before Async Flags
impact: HIGH
impactDescription: avoids unnecessary async work when a synchronous guard already fails
tags: async, await, feature-flags, short-circuit, conditional
---
## Check Cheap Conditions Before Async Flags
When a branch uses `await` for a flag or remote value and also requires a **cheap synchronous** condition (local props, request metadata, already-loaded state), evaluate the cheap condition **first**. Otherwise you pay for the async call even when the compound condition can never be true.
This is a specialization of [Defer Await Until Needed](./async-defer-await.md) for `flag && cheapCondition` style checks.
**Incorrect:**
```typescript
const someFlag = await getFlag();
if (someFlag && someCondition) {
// ...
}
```
**Correct:**
```typescript
if (someCondition) {
const someFlag = await getFlag();
if (someFlag) {
// ...
}
}
```
This matters when `getFlag` hits the network, a feature-flag service, or `React.cache` / DB work: skipping it when `someCondition` is false removes that cost on the cold path.
Keep the original order if `someCondition` is expensive, depends on the flag, or you must run side effects in a fixed order.
@@ -0,0 +1,82 @@
---
title: Defer Await Until Needed
impact: HIGH
impactDescription: avoids blocking unused code paths
tags: async, await, conditional, optimization
---
## Defer Await Until Needed
Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
**Incorrect (blocks both branches):**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId);
if (skipProcessing) {
// Returns immediately but still waited for userData
return { skipped: true };
}
// Only this branch uses userData
return processUserData(userData);
}
```
**Correct (only blocks when needed):**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// Returns immediately without waiting
return { skipped: true };
}
// Fetch only when needed
const userData = await fetchUserData(userId);
return processUserData(userData);
}
```
**Another example (early return optimization):**
```typescript
// Incorrect: always fetches permissions
async function updateResource(resourceId: string, userId: string) {
const permissions = await fetchPermissions(userId);
const resource = await getResource(resourceId);
if (!resource) {
return { error: "Not found" };
}
if (!permissions.canEdit) {
return { error: "Forbidden" };
}
return await updateResourceData(resource, permissions);
}
// Correct: fetches only when needed
async function updateResource(resourceId: string, userId: string) {
const resource = await getResource(resourceId);
if (!resource) {
return { error: "Not found" };
}
const permissions = await fetchPermissions(userId);
if (!permissions.canEdit) {
return { error: "Forbidden" };
}
return await updateResourceData(resource, permissions);
}
```
This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
For `await getFlag()` combined with a cheap synchronous guard (`flag && someCondition`), see [Check Cheap Conditions Before Async Flags](./async-cheap-condition-before-await.md).
@@ -0,0 +1,52 @@
---
title: Dependency-Based Parallelization
impact: CRITICAL
impactDescription: 2-10× improvement
tags: async, parallelization, dependencies, better-all
---
## Dependency-Based Parallelization
For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
**Incorrect (profile waits for config unnecessarily):**
```typescript
const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
const profile = await fetchProfile(user.id);
```
**Correct (config and profile run in parallel):**
```typescript
import { all } from "better-all";
const { user, config, profile } = await all({
async user() {
return fetchUser();
},
async config() {
return fetchConfig();
},
async profile() {
return fetchProfile((await this.$.user).id);
},
});
```
**Alternative without extra dependencies:**
We can also create all the promises first, and do `Promise.all()` at the end.
```typescript
const userPromise = fetchUser();
const profilePromise = userPromise.then((user) => fetchProfile(user.id));
const [user, config, profile] = await Promise.all([
userPromise,
fetchConfig(),
profilePromise,
]);
```
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
@@ -0,0 +1,28 @@
---
title: Promise.all() for Independent Operations
impact: CRITICAL
impactDescription: 2-10× improvement
tags: async, parallelization, promises, waterfalls
---
## Promise.all() for Independent Operations
When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
**Incorrect (sequential execution, 3 round trips):**
```typescript
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();
```
**Correct (parallel execution, 1 round trip):**
```typescript
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
```
@@ -0,0 +1,99 @@
---
title: Strategic Suspense Boundaries
impact: HIGH
impactDescription: faster initial paint
tags: async, suspense, streaming, layout-shift
---
## Strategic Suspense Boundaries
Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
**Incorrect (wrapper blocked by data fetching):**
```tsx
async function Page() {
const data = await fetchData(); // Blocks entire page
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<DataDisplay data={data} />
</div>
<div>Footer</div>
</div>
);
}
```
The entire layout waits for data even though only the middle section needs it.
**Correct (wrapper shows immediately, data streams in):**
```tsx
function Page() {
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
</div>
<div>Footer</div>
</div>
);
}
async function DataDisplay() {
const data = await fetchData(); // Only blocks this component
return <div>{data.content}</div>;
}
```
Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
**Alternative (share promise across components):**
```tsx
function Page() {
// Start fetch immediately, but don't await
const dataPromise = fetchData();
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<Suspense fallback={<Skeleton />}>
<DataDisplay dataPromise={dataPromise} />
<DataSummary dataPromise={dataPromise} />
</Suspense>
<div>Footer</div>
</div>
);
}
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise); // Unwraps the promise
return <div>{data.content}</div>;
}
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise); // Reuses the same promise
return <div>{data.summary}</div>;
}
```
Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
**When NOT to use this pattern:**
- Critical data needed for layout decisions (affects positioning)
- SEO-critical content above the fold
- Small, fast queries where suspense overhead isn't worth it
- When you want to avoid layout shift (loading → content jump)
**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
@@ -0,0 +1,60 @@
---
title: Avoid Barrel File Imports
impact: CRITICAL
impactDescription: 200-800ms import cost, slow builds
tags: bundle, imports, tree-shaking, barrel-files, performance
---
## Avoid Barrel File Imports
Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
**Incorrect (imports entire library):**
```tsx
import { Check, X, Menu } from "lucide-react";
// Loads 1,583 modules, takes ~2.8s extra in dev
// Runtime cost: 200-800ms on every cold start
import { Button, TextField } from "@mui/material";
// Loads 2,225 modules, takes ~4.2s extra in dev
```
**Correct - Next.js 13.5+ (recommended):**
```js
// next.config.js - automatically optimizes barrel imports at build time
module.exports = {
experimental: {
optimizePackageImports: ["lucide-react", "@mui/material"],
},
};
```
```tsx
// Keep the standard imports - Next.js transforms them to direct imports
import { Check, X, Menu } from "lucide-react";
// Full TypeScript support, no manual path wrangling
```
This is the recommended approach because it preserves TypeScript type safety and editor autocompletion while still eliminating the barrel import cost.
**Correct - Direct imports (non-Next.js projects):**
```tsx
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
// Loads only what you use
```
> **TypeScript warning:** Some libraries (notably `lucide-react`) don't ship `.d.ts` files for their deep import paths. Importing from `lucide-react/dist/esm/icons/check` resolves to an implicit `any` type, causing errors under `strict` or `noImplicitAny`. Prefer `optimizePackageImports` when available, or verify the library exports types for its subpaths before using direct imports.
These optimizations provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
@@ -0,0 +1,37 @@
---
title: Conditional Module Loading
impact: HIGH
impactDescription: loads large data only when needed
tags: bundle, conditional-loading, lazy-loading
---
## Conditional Module Loading
Load large data or modules only when a feature is activated.
**Example (lazy-load animation frames):**
```tsx
function AnimationPlayer({
enabled,
setEnabled,
}: {
enabled: boolean;
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const [frames, setFrames] = useState<Frame[] | null>(null);
useEffect(() => {
if (enabled && !frames && typeof window !== "undefined") {
import("./animation-frames.js")
.then((mod) => setFrames(mod.frames))
.catch(() => setEnabled(false));
}
}, [enabled, frames, setEnabled]);
if (!frames) return <Skeleton />;
return <Canvas frames={frames} />;
}
```
The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
@@ -0,0 +1,49 @@
---
title: Defer Non-Critical Third-Party Libraries
impact: MEDIUM
impactDescription: loads after hydration
tags: bundle, third-party, analytics, defer
---
## Defer Non-Critical Third-Party Libraries
Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
**Incorrect (blocks initial bundle):**
```tsx
import { Analytics } from "@vercel/analytics/react";
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
);
}
```
**Correct (loads after hydration):**
```tsx
import dynamic from "next/dynamic";
const Analytics = dynamic(
() => import("@vercel/analytics/react").then((m) => m.Analytics),
{ ssr: false },
);
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
);
}
```
@@ -0,0 +1,35 @@
---
title: Dynamic Imports for Heavy Components
impact: CRITICAL
impactDescription: directly affects TTI and LCP
tags: bundle, dynamic-import, code-splitting, next-dynamic
---
## Dynamic Imports for Heavy Components
Use `next/dynamic` to lazy-load large components not needed on initial render.
**Incorrect (Monaco bundles with main chunk ~300KB):**
```tsx
import { MonacoEditor } from "./monaco-editor";
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />;
}
```
**Correct (Monaco loads on demand):**
```tsx
import dynamic from "next/dynamic";
const MonacoEditor = dynamic(
() => import("./monaco-editor").then((m) => m.MonacoEditor),
{ ssr: false },
);
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />;
}
```
@@ -0,0 +1,46 @@
---
title: Preload Based on User Intent
impact: MEDIUM
impactDescription: reduces perceived latency
tags: bundle, preload, user-intent, hover
---
## Preload Based on User Intent
Preload heavy bundles before they're needed to reduce perceived latency.
**Example (preload on hover/focus):**
```tsx
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== "undefined") {
void import("./monaco-editor");
}
};
return (
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
Open Editor
</button>
);
}
```
**Example (preload when feature flag is enabled):**
```tsx
function FlagsProvider({ children, flags }: Props) {
useEffect(() => {
if (flags.editorEnabled && typeof window !== "undefined") {
void import("./monaco-editor").then((mod) => mod.init());
}
}, [flags.editorEnabled]);
return (
<FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>
);
}
```
The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
@@ -0,0 +1,78 @@
---
title: Deduplicate Global Event Listeners
impact: LOW
impactDescription: single listener for N components
tags: client, swr, event-listeners, subscription
---
## Deduplicate Global Event Listeners
Use `useSWRSubscription()` to share global event listeners across component instances.
**Incorrect (N instances = N listeners):**
```tsx
function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === key) {
callback();
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [key, callback]);
}
```
When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
**Correct (N instances = 1 listener):**
```tsx
import useSWRSubscription from "swr/subscription";
// Module-level Map to track callbacks per key
const keyCallbacks = new Map<string, Set<() => void>>();
function useKeyboardShortcut(key: string, callback: () => void) {
// Register this callback in the Map
useEffect(() => {
if (!keyCallbacks.has(key)) {
keyCallbacks.set(key, new Set());
}
keyCallbacks.get(key)!.add(callback);
return () => {
const set = keyCallbacks.get(key);
if (set) {
set.delete(callback);
if (set.size === 0) {
keyCallbacks.delete(key);
}
}
};
}, [key, callback]);
useSWRSubscription("global-keydown", () => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && keyCallbacks.has(e.key)) {
keyCallbacks.get(e.key)!.forEach((cb) => cb());
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
});
}
function Profile() {
// Multiple shortcuts will share the same listener
useKeyboardShortcut("p", () => {
/* ... */
});
useKeyboardShortcut("k", () => {
/* ... */
});
// ...
}
```
@@ -0,0 +1,77 @@
---
title: Version and Minimize localStorage Data
impact: MEDIUM
impactDescription: prevents schema conflicts, reduces storage size
tags: client, localStorage, storage, versioning, data-minimization
---
## Version and Minimize localStorage Data
Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.
**Incorrect:**
```typescript
// No version, stores everything, no error handling
localStorage.setItem("userConfig", JSON.stringify(fullUserObject));
const data = localStorage.getItem("userConfig");
```
**Correct:**
```typescript
const VERSION = "v2";
function saveConfig(config: { theme: string; language: string }) {
try {
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config));
} catch {
// Throws in incognito/private browsing, quota exceeded, or disabled
}
}
function loadConfig() {
try {
const data = localStorage.getItem(`userConfig:${VERSION}`);
return data ? JSON.parse(data) : null;
} catch {
return null;
}
}
// Migration from v1 to v2
function migrate() {
try {
const v1 = localStorage.getItem("userConfig:v1");
if (v1) {
const old = JSON.parse(v1);
saveConfig({
theme: old.darkMode ? "dark" : "light",
language: old.lang,
});
localStorage.removeItem("userConfig:v1");
}
} catch {}
}
```
**Store minimal fields from server responses:**
```typescript
// User object has 20+ fields, only store what UI needs
function cachePrefs(user: FullUser) {
try {
localStorage.setItem(
"prefs:v1",
JSON.stringify({
theme: user.preferences.theme,
notifications: user.preferences.notifications,
}),
);
} catch {}
}
```
**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.
**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.
@@ -0,0 +1,48 @@
---
title: Use Passive Event Listeners for Scrolling Performance
impact: MEDIUM
impactDescription: eliminates scroll delay caused by event listeners
tags: client, event-listeners, scrolling, performance, touch, wheel
---
## Use Passive Event Listeners for Scrolling Performance
Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.
**Incorrect:**
```typescript
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
document.addEventListener("touchstart", handleTouch);
document.addEventListener("wheel", handleWheel);
return () => {
document.removeEventListener("touchstart", handleTouch);
document.removeEventListener("wheel", handleWheel);
};
}, []);
```
**Correct:**
```typescript
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
document.addEventListener("touchstart", handleTouch, { passive: true });
document.addEventListener("wheel", handleWheel, { passive: true });
return () => {
document.removeEventListener("touchstart", handleTouch);
document.removeEventListener("wheel", handleWheel);
};
}, []);
```
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.
@@ -0,0 +1,56 @@
---
title: Use SWR for Automatic Deduplication
impact: MEDIUM-HIGH
impactDescription: automatic deduplication
tags: client, swr, deduplication, data-fetching
---
## Use SWR for Automatic Deduplication
SWR enables request deduplication, caching, and revalidation across component instances.
**Incorrect (no deduplication, each instance fetches):**
```tsx
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users")
.then((r) => r.json())
.then(setUsers);
}, []);
}
```
**Correct (multiple instances share one request):**
```tsx
import useSWR from "swr";
function UserList() {
const { data: users } = useSWR("/api/users", fetcher);
}
```
**For immutable data:**
```tsx
import { useImmutableSWR } from "@/lib/swr";
function StaticContent() {
const { data } = useImmutableSWR("/api/config", fetcher);
}
```
**For mutations:**
```tsx
import { useSWRMutation } from "swr/mutation";
function UpdateButton() {
const { trigger } = useSWRMutation("/api/user", updateUser);
return <button onClick={() => trigger()}>Update</button>;
}
```
Reference: [https://swr.vercel.app](https://swr.vercel.app)
@@ -0,0 +1,110 @@
---
title: Avoid Layout Thrashing
impact: MEDIUM
impactDescription: prevents forced synchronous layouts and reduces performance bottlenecks
tags: javascript, dom, css, performance, reflow, layout-thrashing
---
## Avoid Layout Thrashing
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
**This is OK (browser batches style changes):**
```typescript
function updateElementStyles(element: HTMLElement) {
// Each line invalidates style, but browser batches the recalculation
element.style.width = "100px";
element.style.height = "200px";
element.style.backgroundColor = "blue";
element.style.border = "1px solid black";
}
```
**Incorrect (interleaved reads and writes force reflows):**
```typescript
function layoutThrashing(element: HTMLElement) {
element.style.width = "100px";
const width = element.offsetWidth; // Forces reflow
element.style.height = "200px";
const height = element.offsetHeight; // Forces another reflow
}
```
**Correct (batch writes, then read once):**
```typescript
function updateElementStyles(element: HTMLElement) {
// Batch all writes together
element.style.width = "100px";
element.style.height = "200px";
element.style.backgroundColor = "blue";
element.style.border = "1px solid black";
// Read after all writes are done (single reflow)
const { width, height } = element.getBoundingClientRect();
}
```
**Correct (batch reads, then writes):**
```typescript
function avoidThrashing(element: HTMLElement) {
// Read phase - all layout queries first
const rect1 = element.getBoundingClientRect();
const offsetWidth = element.offsetWidth;
const offsetHeight = element.offsetHeight;
// Write phase - all style changes after
element.style.width = "100px";
element.style.height = "200px";
}
```
**Better: use CSS classes**
```css
.highlighted-box {
width: 100px;
height: 200px;
background-color: blue;
border: 1px solid black;
}
```
```typescript
function updateElementStyles(element: HTMLElement) {
element.classList.add("highlighted-box");
const { width, height } = element.getBoundingClientRect();
}
```
**React example:**
```tsx
// Incorrect: interleaving style changes with layout queries
function Box({ isHighlighted }: { isHighlighted: boolean }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (ref.current && isHighlighted) {
ref.current.style.width = "100px";
const width = ref.current.offsetWidth; // Forces layout
ref.current.style.height = "200px";
}
}, [isHighlighted]);
return <div ref={ref}>Content</div>;
}
// Correct: toggle class
function Box({ isHighlighted }: { isHighlighted: boolean }) {
return <div className={isHighlighted ? "highlighted-box" : ""}>Content</div>;
}
```
Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
@@ -0,0 +1,80 @@
---
title: Cache Repeated Function Calls
impact: MEDIUM
impactDescription: avoid redundant computation
tags: javascript, cache, memoization, performance
---
## Cache Repeated Function Calls
Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
**Incorrect (redundant computation):**
```typescript
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
// slugify() called 100+ times for same project names
const slug = slugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
)
}
```
**Correct (cached results):**
```typescript
// Module-level cache
const slugifyCache = new Map<string, string>()
function cachedSlugify(text: string): string {
if (slugifyCache.has(text)) {
return slugifyCache.get(text)!
}
const result = slugify(text)
slugifyCache.set(text, result)
return result
}
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
// Computed only once per unique project name
const slug = cachedSlugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
)
}
```
**Simpler pattern for single-value functions:**
```typescript
let isLoggedInCache: boolean | null = null;
function isLoggedIn(): boolean {
if (isLoggedInCache !== null) {
return isLoggedInCache;
}
isLoggedInCache = document.cookie.includes("auth=");
return isLoggedInCache;
}
// Clear cache when auth changes
function onAuthChange() {
isLoggedInCache = null;
}
```
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
@@ -0,0 +1,28 @@
---
title: Cache Property Access in Loops
impact: LOW-MEDIUM
impactDescription: reduces lookups
tags: javascript, loops, optimization, caching
---
## Cache Property Access in Loops
Cache object property lookups in hot paths.
**Incorrect (3 lookups × N iterations):**
```typescript
for (let i = 0; i < arr.length; i++) {
process(obj.config.settings.value);
}
```
**Correct (1 lookup total):**
```typescript
const value = obj.config.settings.value;
const len = arr.length;
for (let i = 0; i < len; i++) {
process(value);
}
```
@@ -0,0 +1,70 @@
---
title: Cache Storage API Calls
impact: LOW-MEDIUM
impactDescription: reduces expensive I/O
tags: javascript, localStorage, storage, caching, performance
---
## Cache Storage API Calls
`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
**Incorrect (reads storage on every call):**
```typescript
function getTheme() {
return localStorage.getItem("theme") ?? "light";
}
// Called 10 times = 10 storage reads
```
**Correct (Map cache):**
```typescript
const storageCache = new Map<string, string | null>();
function getLocalStorage(key: string) {
if (!storageCache.has(key)) {
storageCache.set(key, localStorage.getItem(key));
}
return storageCache.get(key);
}
function setLocalStorage(key: string, value: string) {
localStorage.setItem(key, value);
storageCache.set(key, value); // keep cache in sync
}
```
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
**Cookie caching:**
```typescript
let cookieCache: Record<string, string> | null = null;
function getCookie(name: string) {
if (!cookieCache) {
cookieCache = Object.fromEntries(
document.cookie.split("; ").map((c) => c.split("=")),
);
}
return cookieCache[name];
}
```
**Important (invalidate on external changes):**
If storage can change externally (another tab, server-set cookies), invalidate cache:
```typescript
window.addEventListener("storage", (e) => {
if (e.key) storageCache.delete(e.key);
});
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
storageCache.clear();
}
});
```
@@ -0,0 +1,32 @@
---
title: Combine Multiple Array Iterations
impact: LOW-MEDIUM
impactDescription: reduces iterations
tags: javascript, arrays, loops, performance
---
## Combine Multiple Array Iterations
Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
**Incorrect (3 iterations):**
```typescript
const admins = users.filter((u) => u.isAdmin);
const testers = users.filter((u) => u.isTester);
const inactive = users.filter((u) => !u.isActive);
```
**Correct (1 iteration):**
```typescript
const admins: User[] = [];
const testers: User[] = [];
const inactive: User[] = [];
for (const user of users) {
if (user.isAdmin) admins.push(user);
if (user.isTester) testers.push(user);
if (!user.isActive) inactive.push(user);
}
```
@@ -0,0 +1,50 @@
---
title: Early Return from Functions
impact: LOW-MEDIUM
impactDescription: avoids unnecessary computation
tags: javascript, functions, optimization, early-return
---
## Early Return from Functions
Return early when result is determined to skip unnecessary processing.
**Incorrect (processes all items even after finding answer):**
```typescript
function validateUsers(users: User[]) {
let hasError = false;
let errorMessage = "";
for (const user of users) {
if (!user.email) {
hasError = true;
errorMessage = "Email required";
}
if (!user.name) {
hasError = true;
errorMessage = "Name required";
}
// Continues checking all users even after error found
}
return hasError ? { valid: false, error: errorMessage } : { valid: true };
}
```
**Correct (returns immediately on first error):**
```typescript
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) {
return { valid: false, error: "Email required" };
}
if (!user.name) {
return { valid: false, error: "Name required" };
}
}
return { valid: true };
}
```
@@ -0,0 +1,55 @@
---
title: Use flatMap to Map and Filter in One Pass
impact: LOW-MEDIUM
impactDescription: eliminates intermediate array
tags: javascript, arrays, flatMap, filter, performance
---
## Use flatMap to Map and Filter in One Pass
**Impact: LOW-MEDIUM (eliminates intermediate array)**
Chaining `.map().filter(Boolean)` creates an intermediate array and iterates twice. Use `.flatMap()` to transform and filter in a single pass.
**Incorrect (2 iterations, intermediate array):**
```typescript
const userNames = users
.map((user) => (user.isActive ? user.name : null))
.filter(Boolean);
```
**Correct (1 iteration, no intermediate array):**
```typescript
const userNames = users.flatMap((user) => (user.isActive ? [user.name] : []));
```
**More examples:**
```typescript
// Extract valid emails from responses
// Before
const emails = responses
.map((r) => (r.success ? r.data.email : null))
.filter(Boolean);
// After
const emails = responses.flatMap((r) => (r.success ? [r.data.email] : []));
// Parse and filter valid numbers
// Before
const numbers = strings.map((s) => parseInt(s, 10)).filter((n) => !isNaN(n));
// After
const numbers = strings.flatMap((s) => {
const n = parseInt(s, 10);
return isNaN(n) ? [] : [n];
});
```
**When to use:**
- Transforming items while filtering some out
- Conditional mapping where some inputs produce no output
- Parsing/validating where invalid inputs should be skipped
@@ -0,0 +1,45 @@
---
title: Hoist RegExp Creation
impact: LOW-MEDIUM
impactDescription: avoids recreation
tags: javascript, regexp, optimization, memoization
---
## Hoist RegExp Creation
Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
**Incorrect (new RegExp every render):**
```tsx
function Highlighter({ text, query }: Props) {
const regex = new RegExp(`(${query})`, 'gi')
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
```
**Correct (memoize or hoist):**
```tsx
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
function Highlighter({ text, query }: Props) {
const regex = useMemo(
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
[query]
)
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
```
**Warning (global regex has mutable state):**
Global regex (`/g`) has mutable `lastIndex` state:
```typescript
const regex = /foo/g;
regex.test("foo"); // true, lastIndex = 3
regex.test("foo"); // false, lastIndex = 0
```
@@ -0,0 +1,37 @@
---
title: Build Index Maps for Repeated Lookups
impact: LOW-MEDIUM
impactDescription: 1M ops to 2K ops
tags: javascript, map, indexing, optimization, performance
---
## Build Index Maps for Repeated Lookups
Multiple `.find()` calls by the same key should use a Map.
**Incorrect (O(n) per lookup):**
```typescript
function processOrders(orders: Order[], users: User[]) {
return orders.map((order) => ({
...order,
user: users.find((u) => u.id === order.userId),
}));
}
```
**Correct (O(1) per lookup):**
```typescript
function processOrders(orders: Order[], users: User[]) {
const userById = new Map(users.map((u) => [u.id, u]));
return orders.map((order) => ({
...order,
user: userById.get(order.userId),
}));
}
```
Build map once (O(n)), then all lookups are O(1).
For 1000 orders × 1000 users: 1M ops → 2K ops.
@@ -0,0 +1,50 @@
---
title: Early Length Check for Array Comparisons
impact: MEDIUM-HIGH
impactDescription: avoids expensive operations when lengths differ
tags: javascript, arrays, performance, optimization, comparison
---
## Early Length Check for Array Comparisons
When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
**Incorrect (always runs expensive comparison):**
```typescript
function hasChanges(current: string[], original: string[]) {
// Always sorts and joins, even when lengths differ
return current.sort().join() !== original.sort().join();
}
```
Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.
**Correct (O(1) length check first):**
```typescript
function hasChanges(current: string[], original: string[]) {
// Early return if lengths differ
if (current.length !== original.length) {
return true;
}
// Only sort when lengths match
const currentSorted = current.toSorted();
const originalSorted = original.toSorted();
for (let i = 0; i < currentSorted.length; i++) {
if (currentSorted[i] !== originalSorted[i]) {
return true;
}
}
return false;
}
```
This new approach is more efficient because:
- It avoids the overhead of sorting and joining the arrays when lengths differ
- It avoids consuming memory for the joined strings (especially important for large arrays)
- It avoids mutating the original arrays
- It returns early when a difference is found
@@ -0,0 +1,82 @@
---
title: Use Loop for Min/Max Instead of Sort
impact: LOW
impactDescription: O(n) instead of O(n log n)
tags: javascript, arrays, performance, sorting, algorithms
---
## Use Loop for Min/Max Instead of Sort
Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
**Incorrect (O(n log n) - sort to find latest):**
```typescript
interface Project {
id: string;
name: string;
updatedAt: number;
}
function getLatestProject(projects: Project[]) {
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt);
return sorted[0];
}
```
Sorts the entire array just to find the maximum value.
**Incorrect (O(n log n) - sort for oldest and newest):**
```typescript
function getOldestAndNewest(projects: Project[]) {
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt);
return { oldest: sorted[0], newest: sorted[sorted.length - 1] };
}
```
Still sorts unnecessarily when only min/max are needed.
**Correct (O(n) - single loop):**
```typescript
function getLatestProject(projects: Project[]) {
if (projects.length === 0) return null;
let latest = projects[0];
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt > latest.updatedAt) {
latest = projects[i];
}
}
return latest;
}
function getOldestAndNewest(projects: Project[]) {
if (projects.length === 0) return { oldest: null, newest: null };
let oldest = projects[0];
let newest = projects[0];
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i];
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i];
}
return { oldest, newest };
}
```
Single pass through the array, no copying, no sorting.
**Alternative (Math.min/Math.max for small arrays):**
```typescript
const numbers = [5, 2, 8, 1, 9];
const min = Math.min(...numbers);
const max = Math.max(...numbers);
```
This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.
@@ -0,0 +1,106 @@
---
title: Defer Non-Critical Work with requestIdleCallback
impact: MEDIUM
impactDescription: keeps UI responsive during background tasks
tags: javascript, performance, idle, scheduling, analytics
---
## Defer Non-Critical Work with requestIdleCallback
**Impact: MEDIUM (keeps UI responsive during background tasks)**
Use `requestIdleCallback()` to schedule non-critical work during browser idle periods. This keeps the main thread free for user interactions and animations, reducing jank and improving perceived performance.
**Incorrect (blocks main thread during user interaction):**
```typescript
function handleSearch(query: string) {
const results = searchItems(query);
setResults(results);
// These block the main thread immediately
analytics.track("search", { query });
saveToRecentSearches(query);
prefetchTopResults(results.slice(0, 3));
}
```
**Correct (defers non-critical work to idle time):**
```typescript
function handleSearch(query: string) {
const results = searchItems(query);
setResults(results);
// Defer non-critical work to idle periods
requestIdleCallback(() => {
analytics.track("search", { query });
});
requestIdleCallback(() => {
saveToRecentSearches(query);
});
requestIdleCallback(() => {
prefetchTopResults(results.slice(0, 3));
});
}
```
**With timeout for required work:**
```typescript
// Ensure analytics fires within 2 seconds even if browser stays busy
requestIdleCallback(
() => analytics.track("page_view", { path: location.pathname }),
{ timeout: 2000 },
);
```
**Chunking large tasks:**
```typescript
function processLargeDataset(items: Item[]) {
let index = 0;
function processChunk(deadline: IdleDeadline) {
// Process items while we have idle time (aim for <50ms chunks)
while (index < items.length && deadline.timeRemaining() > 0) {
processItem(items[index]);
index++;
}
// Schedule next chunk if more items remain
if (index < items.length) {
requestIdleCallback(processChunk);
}
}
requestIdleCallback(processChunk);
}
```
**With fallback for unsupported browsers:**
```typescript
const scheduleIdleWork =
window.requestIdleCallback ?? ((cb: () => void) => setTimeout(cb, 1));
scheduleIdleWork(() => {
// Non-critical work
});
```
**When to use:**
- Analytics and telemetry
- Saving state to localStorage/IndexedDB
- Prefetching resources for likely next actions
- Processing non-urgent data transformations
- Lazy initialization of non-critical features
**When NOT to use:**
- User-initiated actions that need immediate feedback
- Rendering updates the user is waiting for
- Time-sensitive operations
@@ -0,0 +1,24 @@
---
title: Use Set/Map for O(1) Lookups
impact: LOW-MEDIUM
impactDescription: O(n) to O(1)
tags: javascript, set, map, data-structures, performance
---
## Use Set/Map for O(1) Lookups
Convert arrays to Set/Map for repeated membership checks.
**Incorrect (O(n) per check):**
```typescript
const allowedIds = ['a', 'b', 'c', ...]
items.filter(item => allowedIds.includes(item.id))
```
**Correct (O(1) per check):**
```typescript
const allowedIds = new Set(['a', 'b', 'c', ...])
items.filter(item => allowedIds.has(item.id))
```
@@ -0,0 +1,57 @@
---
title: Use toSorted() Instead of sort() for Immutability
impact: MEDIUM-HIGH
impactDescription: prevents mutation bugs in React state
tags: javascript, arrays, immutability, react, state, mutation
---
## Use toSorted() Instead of sort() for Immutability
`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.
**Incorrect (mutates original array):**
```typescript
function UserList({ users }: { users: User[] }) {
// Mutates the users prop array!
const sorted = useMemo(
() => users.sort((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
}
```
**Correct (creates new array):**
```typescript
function UserList({ users }: { users: User[] }) {
// Creates new sorted array, original unchanged
const sorted = useMemo(
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
}
```
**Why this matters in React:**
1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
**Browser support (fallback for older browsers):**
`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
```typescript
// Fallback for older browsers
const sorted = [...items].sort((a, b) => a.value - b.value);
```
**Other immutable array methods:**
- `.toSorted()` - immutable sort
- `.toReversed()` - immutable reverse
- `.toSpliced()` - immutable splice
- `.with()` - immutable element replacement
@@ -0,0 +1,26 @@
---
title: Use Activity Component for Show/Hide
impact: MEDIUM
impactDescription: preserves state/DOM
tags: rendering, activity, visibility, state-preservation
---
## Use Activity Component for Show/Hide
Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
**Usage:**
```tsx
import { Activity } from "react";
function Dropdown({ isOpen }: Props) {
return (
<Activity mode={isOpen ? "visible" : "hidden"}>
<ExpensiveMenu />
</Activity>
);
}
```
Avoids expensive re-renders and state loss.
@@ -0,0 +1,38 @@
---
title: Animate SVG Wrapper Instead of SVG Element
impact: LOW
impactDescription: enables hardware acceleration
tags: rendering, svg, css, animation, performance
---
## Animate SVG Wrapper Instead of SVG Element
Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
**Incorrect (animating SVG directly - no hardware acceleration):**
```tsx
function LoadingSpinner() {
return (
<svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
);
}
```
**Correct (animating wrapper div - hardware accelerated):**
```tsx
function LoadingSpinner() {
return (
<div className="animate-spin">
<svg width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
</div>
);
}
```
This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
@@ -0,0 +1,32 @@
---
title: Use Explicit Conditional Rendering
impact: LOW
impactDescription: prevents rendering 0 or NaN
tags: rendering, conditional, jsx, falsy-values
---
## Use Explicit Conditional Rendering
Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
**Incorrect (renders "0" when count is 0):**
```tsx
function Badge({ count }: { count: number }) {
return <div>{count && <span className="badge">{count}</span>}</div>;
}
// When count = 0, renders: <div>0</div>
// When count = 5, renders: <div><span class="badge">5</span></div>
```
**Correct (renders nothing when count is 0):**
```tsx
function Badge({ count }: { count: number }) {
return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>;
}
// When count = 0, renders: <div></div>
// When count = 5, renders: <div><span class="badge">5</span></div>
```
@@ -0,0 +1,38 @@
---
title: CSS content-visibility for Long Lists
impact: HIGH
impactDescription: faster initial render
tags: rendering, css, content-visibility, long-lists
---
## CSS content-visibility for Long Lists
Apply `content-visibility: auto` to defer off-screen rendering.
**CSS:**
```css
.message-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
```
**Example:**
```tsx
function MessageList({ messages }: { messages: Message[] }) {
return (
<div className="overflow-y-auto h-screen">
{messages.map((msg) => (
<div key={msg.id} className="message-item">
<Avatar user={msg.author} />
<div>{msg.content}</div>
</div>
))}
</div>
);
}
```
For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
@@ -0,0 +1,36 @@
---
title: Hoist Static JSX Elements
impact: LOW
impactDescription: avoids re-creation
tags: rendering, jsx, static, optimization
---
## Hoist Static JSX Elements
Extract static JSX outside components to avoid re-creation.
**Incorrect (recreates element every render):**
```tsx
function LoadingSkeleton() {
return <div className="animate-pulse h-20 bg-gray-200" />;
}
function Container() {
return <div>{loading && <LoadingSkeleton />}</div>;
}
```
**Correct (reuses same element):**
```tsx
const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />;
function Container() {
return <div>{loading && loadingSkeleton}</div>;
}
```
This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
@@ -0,0 +1,72 @@
---
title: Prevent Hydration Mismatch Without Flickering
impact: MEDIUM
impactDescription: avoids visual flicker and hydration errors
tags: rendering, ssr, hydration, localStorage, flicker
---
## Prevent Hydration Mismatch Without Flickering
When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
**Incorrect (breaks SSR):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
// localStorage is not available on server - throws error
const theme = localStorage.getItem("theme") || "light";
return <div className={theme}>{children}</div>;
}
```
Server-side rendering will fail because `localStorage` is undefined.
**Incorrect (visual flickering):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState("light");
useEffect(() => {
// Runs after hydration - causes visible flash
const stored = localStorage.getItem("theme");
if (stored) {
setTheme(stored);
}
}, []);
return <div className={theme}>{children}</div>;
}
```
Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
**Correct (no flicker, no hydration mismatch):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
return (
<>
<div id="theme-wrapper">{children}</div>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var theme = localStorage.getItem('theme') || 'light';
var el = document.getElementById('theme-wrapper');
if (el) el.className = theme;
} catch (e) {}
})();
`,
}}
/>
</>
);
}
```
The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
@@ -0,0 +1,26 @@
---
title: Suppress Expected Hydration Mismatches
impact: LOW-MEDIUM
impactDescription: avoids noisy hydration warnings for known differences
tags: rendering, hydration, ssr, nextjs
---
## Suppress Expected Hydration Mismatches
In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these _expected_ mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Dont overuse it.
**Incorrect (known mismatch warnings):**
```tsx
function Timestamp() {
return <span>{new Date().toLocaleString()}</span>;
}
```
**Correct (suppress expected mismatch only):**
```tsx
function Timestamp() {
return <span suppressHydrationWarning>{new Date().toLocaleString()}</span>;
}
```
@@ -0,0 +1,89 @@
---
title: Use React DOM Resource Hints
impact: HIGH
impactDescription: reduces load time for critical resources
tags: rendering, preload, preconnect, prefetch, resource-hints
---
## Use React DOM Resource Hints
**Impact: HIGH (reduces load time for critical resources)**
React DOM provides APIs to hint the browser about resources it will need. These are especially useful in server components to start loading resources before the client even receives the HTML.
- **`prefetchDNS(href)`**: Resolve DNS for a domain you expect to connect to
- **`preconnect(href)`**: Establish connection (DNS + TCP + TLS) to a server
- **`preload(href, options)`**: Fetch a resource (stylesheet, font, script, image) you'll use soon
- **`preloadModule(href)`**: Fetch an ES module you'll use soon
- **`preinit(href, options)`**: Fetch and evaluate a stylesheet or script
- **`preinitModule(href)`**: Fetch and evaluate an ES module
**Example (preconnect to third-party APIs):**
```tsx
import { preconnect, prefetchDNS } from "react-dom";
export default function App() {
prefetchDNS("https://analytics.example.com");
preconnect("https://api.example.com");
return <main>{/* content */}</main>;
}
```
**Example (preload critical fonts and styles):**
```tsx
import { preload, preinit } from "react-dom";
export default function RootLayout({ children }) {
// Preload font file
preload("/fonts/inter.woff2", {
as: "font",
type: "font/woff2",
crossOrigin: "anonymous",
});
// Fetch and apply critical stylesheet immediately
preinit("/styles/critical.css", { as: "style" });
return (
<html>
<body>{children}</body>
</html>
);
}
```
**Example (preload modules for code-split routes):**
```tsx
import { preloadModule, preinitModule } from "react-dom";
function Navigation() {
const preloadDashboard = () => {
preloadModule("/dashboard.js", { as: "script" });
};
return (
<nav>
<a href="/dashboard" onMouseEnter={preloadDashboard}>
Dashboard
</a>
</nav>
);
}
```
**When to use each:**
| API | Use case |
| --------------- | ------------------------------------------- |
| `prefetchDNS` | Third-party domains you'll connect to later |
| `preconnect` | APIs or CDNs you'll fetch from immediately |
| `preload` | Critical resources needed for current page |
| `preloadModule` | JS modules for likely next navigation |
| `preinit` | Stylesheets/scripts that must execute early |
| `preinitModule` | ES modules that must execute early |
Reference: [React DOM Resource Preloading APIs](https://react.dev/reference/react-dom#resource-preloading-apis)
@@ -0,0 +1,71 @@
---
title: Use defer or async on Script Tags
impact: HIGH
impactDescription: eliminates render-blocking
tags: rendering, script, defer, async, performance
---
## Use defer or async on Script Tags
**Impact: HIGH (eliminates render-blocking)**
Script tags without `defer` or `async` block HTML parsing while the script downloads and executes. This delays First Contentful Paint and Time to Interactive.
- **`defer`**: Downloads in parallel, executes after HTML parsing completes, maintains execution order
- **`async`**: Downloads in parallel, executes immediately when ready, no guaranteed order
Use `defer` for scripts that depend on DOM or other scripts. Use `async` for independent scripts like analytics.
**Incorrect (blocks rendering):**
```tsx
export default function Document() {
return (
<html>
<head>
<script src="https://example.com/analytics.js" />
<script src="/scripts/utils.js" />
</head>
<body>{/* content */}</body>
</html>
);
}
```
**Correct (non-blocking):**
```tsx
export default function Document() {
return (
<html>
<head>
{/* Independent script - use async */}
<script src="https://example.com/analytics.js" async />
{/* DOM-dependent script - use defer */}
<script src="/scripts/utils.js" defer />
</head>
<body>{/* content */}</body>
</html>
);
}
```
**Note:** In Next.js, prefer the `next/script` component with `strategy` prop instead of raw script tags:
```tsx
import Script from "next/script";
export default function Page() {
return (
<>
<Script
src="https://example.com/analytics.js"
strategy="afterInteractive"
/>
<Script src="/scripts/utils.js" strategy="beforeInteractive" />
</>
);
}
```
Reference: [MDN - Script element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer)
@@ -0,0 +1,28 @@
---
title: Optimize SVG Precision
impact: LOW
impactDescription: reduces file size
tags: rendering, svg, optimization, svgo
---
## Optimize SVG Precision
Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
**Incorrect (excessive precision):**
```svg
<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
```
**Correct (1 decimal place):**
```svg
<path d="M 10.3 20.8 L 30.9 40.2" />
```
**Automate with SVGO:**
```bash
npx svgo --precision=1 --multipass icon.svg
```
@@ -0,0 +1,75 @@
---
title: Use useTransition Over Manual Loading States
impact: LOW
impactDescription: reduces re-renders and improves code clarity
tags: rendering, transitions, useTransition, loading, state
---
## Use useTransition Over Manual Loading States
Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
**Incorrect (manual loading state):**
```tsx
function SearchResults() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const handleSearch = async (value: string) => {
setIsLoading(true);
setQuery(value);
const data = await fetchResults(value);
setResults(data);
setIsLoading(false);
};
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} />
{isLoading && <Spinner />}
<ResultsList results={results} />
</>
);
}
```
**Correct (useTransition with built-in pending state):**
```tsx
import { useTransition, useState } from "react";
function SearchResults() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (value: string) => {
setQuery(value); // Update input immediately
startTransition(async () => {
// Fetch and update results
const data = await fetchResults(value);
setResults(data);
});
};
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<ResultsList results={results} />
</>
);
}
```
**Benefits:**
- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
- **Error resilience**: Pending state correctly resets even if the transition throws
- **Better responsiveness**: Keeps the UI responsive during updates
- **Interrupt handling**: New transitions automatically cancel pending ones
Reference: [useTransition](https://react.dev/reference/react/useTransition)
@@ -0,0 +1,39 @@
---
title: Defer State Reads to Usage Point
impact: MEDIUM
impactDescription: avoids unnecessary subscriptions
tags: rerender, searchParams, localStorage, optimization
---
## Defer State Reads to Usage Point
Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
**Incorrect (subscribes to all searchParams changes):**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams();
const handleShare = () => {
const ref = searchParams.get("ref");
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>;
}
```
**Correct (reads on demand, no subscription):**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search);
const ref = params.get("ref");
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>;
}
```
@@ -0,0 +1,45 @@
---
title: Narrow Effect Dependencies
impact: LOW
impactDescription: minimizes effect re-runs
tags: rerender, useEffect, dependencies, optimization
---
## Narrow Effect Dependencies
Specify primitive dependencies instead of objects to minimize effect re-runs.
**Incorrect (re-runs on any user field change):**
```tsx
useEffect(() => {
console.log(user.id);
}, [user]);
```
**Correct (re-runs only when id changes):**
```tsx
useEffect(() => {
console.log(user.id);
}, [user.id]);
```
**For derived state, compute outside effect:**
```tsx
// Incorrect: runs on width=767, 766, 765...
useEffect(() => {
if (width < 768) {
enableMobileMode();
}
}, [width]);
// Correct: runs only on boolean transition
const isMobile = width < 768;
useEffect(() => {
if (isMobile) {
enableMobileMode();
}
}, [isMobile]);
```
@@ -0,0 +1,40 @@
---
title: Calculate Derived State During Rendering
impact: MEDIUM
impactDescription: avoids redundant renders and state drift
tags: rerender, derived-state, useEffect, state
---
## Calculate Derived State During Rendering
If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.
**Incorrect (redundant state and effect):**
```tsx
function Form() {
const [firstName, setFirstName] = useState("First");
const [lastName, setLastName] = useState("Last");
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(firstName + " " + lastName);
}, [firstName, lastName]);
return <p>{fullName}</p>;
}
```
**Correct (derive during render):**
```tsx
function Form() {
const [firstName, setFirstName] = useState("First");
const [lastName, setLastName] = useState("Last");
const fullName = firstName + " " + lastName;
return <p>{fullName}</p>;
}
```
References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
@@ -0,0 +1,29 @@
---
title: Subscribe to Derived State
impact: MEDIUM
impactDescription: reduces re-render frequency
tags: rerender, derived-state, media-query, optimization
---
## Subscribe to Derived State
Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
**Incorrect (re-renders on every pixel change):**
```tsx
function Sidebar() {
const width = useWindowWidth(); // updates continuously
const isMobile = width < 768;
return <nav className={isMobile ? "mobile" : "desktop"} />;
}
```
**Correct (re-renders only when boolean changes):**
```tsx
function Sidebar() {
const isMobile = useMediaQuery("(max-width: 767px)");
return <nav className={isMobile ? "mobile" : "desktop"} />;
}
```
@@ -0,0 +1,77 @@
---
title: Use Functional setState Updates
impact: MEDIUM
impactDescription: prevents stale closures and unnecessary callback recreations
tags: react, hooks, useState, useCallback, callbacks, closures
---
## Use Functional setState Updates
When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
**Incorrect (requires state as dependency):**
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems);
// Callback must depend on items, recreated on every items change
const addItems = useCallback(
(newItems: Item[]) => {
setItems([...items, ...newItems]);
},
[items],
); // ❌ items dependency causes recreations
// Risk of stale closure if dependency is forgotten
const removeItem = useCallback((id: string) => {
setItems(items.filter((item) => item.id !== id));
}, []); // ❌ Missing items dependency - will use stale items!
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
}
```
The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
**Correct (stable callbacks, no stale closures):**
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems);
// Stable callback, never recreated
const addItems = useCallback((newItems: Item[]) => {
setItems((curr) => [...curr, ...newItems]);
}, []); // ✅ No dependencies needed
// Always uses latest state, no stale closure risk
const removeItem = useCallback((id: string) => {
setItems((curr) => curr.filter((item) => item.id !== id));
}, []); // ✅ Safe and stable
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
}
```
**Benefits:**
1. **Stable callback references** - Callbacks don't need to be recreated when state changes
2. **No stale closures** - Always operates on the latest state value
3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
4. **Prevents bugs** - Eliminates the most common source of React closure bugs
**When to use functional updates:**
- Any setState that depends on the current state value
- Inside useCallback/useMemo when state is needed
- Event handlers that reference state
- Async operations that update state
**When direct updates are fine:**
- Setting state to a static value: `setCount(0)`
- Setting state from props/arguments only: `setName(newName)`
- State doesn't depend on previous value
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
@@ -0,0 +1,58 @@
---
title: Use Lazy State Initialization
impact: MEDIUM
impactDescription: wasted computation on every render
tags: react, hooks, useState, performance, initialization
---
## Use Lazy State Initialization
Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
**Incorrect (runs on every render):**
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs on EVERY render, even after initialization
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items));
const [query, setQuery] = useState("");
// When query changes, buildSearchIndex runs again unnecessarily
return <SearchResults index={searchIndex} query={query} />;
}
function UserProfile() {
// JSON.parse runs on every render
const [settings, setSettings] = useState(
JSON.parse(localStorage.getItem("settings") || "{}"),
);
return <SettingsForm settings={settings} onChange={setSettings} />;
}
```
**Correct (runs only once):**
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs ONLY on initial render
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items));
const [query, setQuery] = useState("");
return <SearchResults index={searchIndex} query={query} />;
}
function UserProfile() {
// JSON.parse runs only on initial render
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem("settings");
return stored ? JSON.parse(stored) : {};
});
return <SettingsForm settings={settings} onChange={setSettings} />;
}
```
Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
@@ -0,0 +1,36 @@
---
title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
impact: MEDIUM
impactDescription: restores memoization by using a constant for default value
tags: rerender, memo, optimization
---
## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
To address this issue, extract the default value into a constant.
**Incorrect (`onClick` has different values on every rerender):**
```tsx
const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
// ...
})
// Used without optional onClick
<UserAvatar />
```
**Correct (stable default value):**
```tsx
const NOOP = () => {};
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
// ...
})
// Used without optional onClick
<UserAvatar />
```
@@ -0,0 +1,44 @@
---
title: Extract to Memoized Components
impact: MEDIUM
impactDescription: enables early returns
tags: rerender, memo, useMemo, optimization
---
## Extract to Memoized Components
Extract expensive work into memoized components to enable early returns before computation.
**Incorrect (computes avatar even when loading):**
```tsx
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => {
const id = computeAvatarId(user);
return <Avatar id={id} />;
}, [user]);
if (loading) return <Skeleton />;
return <div>{avatar}</div>;
}
```
**Correct (skips computation when loading):**
```tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user]);
return <Avatar id={id} />;
});
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />;
return (
<div>
<UserAvatar user={user} />
</div>
);
}
```
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
@@ -0,0 +1,45 @@
---
title: Put Interaction Logic in Event Handlers
impact: MEDIUM
impactDescription: avoids effect re-runs and duplicate side effects
tags: rerender, useEffect, events, side-effects, dependencies
---
## Put Interaction Logic in Event Handlers
If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
**Incorrect (event modeled as state + effect):**
```tsx
function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);
useEffect(() => {
if (submitted) {
post("/api/register");
showToast("Registered", theme);
}
}, [submitted, theme]);
return <button onClick={() => setSubmitted(true)}>Submit</button>;
}
```
**Correct (do it in the handler):**
```tsx
function Form() {
const theme = useContext(ThemeContext);
function handleSubmit() {
post("/api/register");
showToast("Registered", theme);
}
return <button onClick={handleSubmit}>Submit</button>;
}
```
Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
@@ -0,0 +1,83 @@
---
title: Don't Define Components Inside Components
impact: HIGH
impactDescription: prevents remount on every render
tags: rerender, components, remount, performance
---
## Don't Define Components Inside Components
**Impact: HIGH (prevents remount on every render)**
Defining a component inside another component creates a new component type on every render. React sees a different component each time and fully remounts it, destroying all state and DOM.
A common reason developers do this is to access parent variables without passing props. Always pass props instead.
**Incorrect (remounts on every render):**
```tsx
function UserProfile({ user, theme }) {
// Defined inside to access `theme` - BAD
const Avatar = () => (
<img
src={user.avatarUrl}
className={theme === "dark" ? "avatar-dark" : "avatar-light"}
/>
);
// Defined inside to access `user` - BAD
const Stats = () => (
<div>
<span>{user.followers} followers</span>
<span>{user.posts} posts</span>
</div>
);
return (
<div>
<Avatar />
<Stats />
</div>
);
}
```
Every time `UserProfile` renders, `Avatar` and `Stats` are new component types. React unmounts the old instances and mounts new ones, losing any internal state, running effects again, and recreating DOM nodes.
**Correct (pass props instead):**
```tsx
function Avatar({ src, theme }: { src: string; theme: string }) {
return (
<img
src={src}
className={theme === "dark" ? "avatar-dark" : "avatar-light"}
/>
);
}
function Stats({ followers, posts }: { followers: number; posts: number }) {
return (
<div>
<span>{followers} followers</span>
<span>{posts} posts</span>
</div>
);
}
function UserProfile({ user, theme }) {
return (
<div>
<Avatar src={user.avatarUrl} theme={theme} />
<Stats followers={user.followers} posts={user.posts} />
</div>
);
}
```
**Symptoms of this bug:**
- Input fields lose focus on every keystroke
- Animations restart unexpectedly
- `useEffect` cleanup/setup runs on every parent render
- Scroll position resets inside the component
@@ -0,0 +1,35 @@
---
title: Do not wrap a simple expression with a primitive result type in useMemo
impact: LOW-MEDIUM
impactDescription: wasted computation on every render
tags: rerender, useMemo, optimization
---
## Do not wrap a simple expression with a primitive result type in useMemo
When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
**Incorrect:**
```tsx
function Header({ user, notifications }: Props) {
const isLoading = useMemo(() => {
return user.isLoading || notifications.isLoading;
}, [user.isLoading, notifications.isLoading]);
if (isLoading) return <Skeleton />;
// return some markup
}
```
**Correct:**
```tsx
function Header({ user, notifications }: Props) {
const isLoading = user.isLoading || notifications.isLoading;
if (isLoading) return <Skeleton />;
// return some markup
}
```
@@ -0,0 +1,64 @@
---
title: Split Combined Hook Computations
impact: MEDIUM
impactDescription: avoids recomputing independent steps
tags: rerender, useMemo, useEffect, dependencies, optimization
---
## Split Combined Hook Computations
When a hook contains multiple independent tasks with different dependencies, split them into separate hooks. A combined hook reruns all tasks when any dependency changes, even if some tasks don't use the changed value.
**Incorrect (changing `sortOrder` recomputes filtering):**
```tsx
const sortedProducts = useMemo(() => {
const filtered = products.filter((p) => p.category === category);
const sorted = filtered.toSorted((a, b) =>
sortOrder === "asc" ? a.price - b.price : b.price - a.price,
);
return sorted;
}, [products, category, sortOrder]);
```
**Correct (filtering only recomputes when products or category change):**
```tsx
const filteredProducts = useMemo(
() => products.filter((p) => p.category === category),
[products, category],
);
const sortedProducts = useMemo(
() =>
filteredProducts.toSorted((a, b) =>
sortOrder === "asc" ? a.price - b.price : b.price - a.price,
),
[filteredProducts, sortOrder],
);
```
This pattern also applies to `useEffect` when combining unrelated side effects:
**Incorrect (both effects run when either dependency changes):**
```tsx
useEffect(() => {
analytics.trackPageView(pathname);
document.title = `${pageTitle} | My App`;
}, [pathname, pageTitle]);
```
**Correct (effects run independently):**
```tsx
useEffect(() => {
analytics.trackPageView(pathname);
}, [pathname]);
useEffect(() => {
document.title = `${pageTitle} | My App`;
}, [pageTitle]);
```
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, it automatically optimizes dependency tracking and may handle some of these cases for you.
@@ -0,0 +1,40 @@
---
title: Use Transitions for Non-Urgent Updates
impact: MEDIUM
impactDescription: maintains UI responsiveness
tags: rerender, transitions, startTransition, performance
---
## Use Transitions for Non-Urgent Updates
Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
**Incorrect (blocks UI on every scroll):**
```tsx
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => setScrollY(window.scrollY);
window.addEventListener("scroll", handler, { passive: true });
return () => window.removeEventListener("scroll", handler);
}, []);
}
```
**Correct (non-blocking updates):**
```tsx
import { startTransition } from "react";
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY));
};
window.addEventListener("scroll", handler, { passive: true });
return () => window.removeEventListener("scroll", handler);
}, []);
}
```
@@ -0,0 +1,59 @@
---
title: Use useDeferredValue for Expensive Derived Renders
impact: MEDIUM
impactDescription: keeps input responsive during heavy computation
tags: rerender, useDeferredValue, optimization, concurrent
---
## Use useDeferredValue for Expensive Derived Renders
When user input triggers expensive computations or renders, use `useDeferredValue` to keep the input responsive. The deferred value lags behind, allowing React to prioritize the input update and render the expensive result when idle.
**Incorrect (input feels laggy while filtering):**
```tsx
function Search({ items }: { items: Item[] }) {
const [query, setQuery] = useState("");
const filtered = items.filter((item) => fuzzyMatch(item, query));
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ResultsList results={filtered} />
</>
);
}
```
**Correct (input stays snappy, results render when ready):**
```tsx
function Search({ items }: { items: Item[] }) {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const filtered = useMemo(
() => items.filter((item) => fuzzyMatch(item, deferredQuery)),
[items, deferredQuery],
);
const isStale = query !== deferredQuery;
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.7 : 1 }}>
<ResultsList results={filtered} />
</div>
</>
);
}
```
**When to use:**
- Filtering/searching large lists
- Expensive visualizations (charts, graphs) reacting to input
- Any derived state that causes noticeable render delays
**Note:** Wrap the expensive computation in `useMemo` with the deferred value as a dependency, otherwise it still runs on every render.
Reference: [React useDeferredValue](https://react.dev/reference/react/useDeferredValue)
@@ -0,0 +1,73 @@
---
title: Use useRef for Transient Values
impact: MEDIUM
impactDescription: avoids unnecessary re-renders on frequent updates
tags: rerender, useref, state, performance
---
## Use useRef for Transient Values
When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
**Incorrect (renders every update):**
```tsx
function Tracker() {
const [lastX, setLastX] = useState(0);
useEffect(() => {
const onMove = (e: MouseEvent) => setLastX(e.clientX);
window.addEventListener("mousemove", onMove);
return () => window.removeEventListener("mousemove", onMove);
}, []);
return (
<div
style={{
position: "fixed",
top: 0,
left: lastX,
width: 8,
height: 8,
background: "black",
}}
/>
);
}
```
**Correct (no re-render for tracking):**
```tsx
function Tracker() {
const lastXRef = useRef(0);
const dotRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const onMove = (e: MouseEvent) => {
lastXRef.current = e.clientX;
const node = dotRef.current;
if (node) {
node.style.transform = `translateX(${e.clientX}px)`;
}
};
window.addEventListener("mousemove", onMove);
return () => window.removeEventListener("mousemove", onMove);
}, []);
return (
<div
ref={dotRef}
style={{
position: "fixed",
top: 0,
left: 0,
width: 8,
height: 8,
background: "black",
transform: "translateX(0px)",
}}
/>
);
}
```
@@ -0,0 +1,74 @@
---
title: Use after() for Non-Blocking Operations
impact: MEDIUM
impactDescription: faster response times
tags: server, async, logging, analytics, side-effects
---
## Use after() for Non-Blocking Operations
Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
**Incorrect (blocks response):**
```tsx
import { logUserAction } from "@/app/utils";
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request);
// Logging blocks the response
const userAgent = request.headers.get("user-agent") || "unknown";
await logUserAction({ userAgent });
return new Response(JSON.stringify({ status: "success" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
```
**Correct (non-blocking):**
```tsx
import { after } from "next/server";
import { headers, cookies } from "next/headers";
import { logUserAction } from "@/app/utils";
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request);
// Log after response is sent
after(async () => {
const userAgent = (await headers()).get("user-agent") || "unknown";
const sessionCookie =
(await cookies()).get("session-id")?.value || "anonymous";
logUserAction({ sessionCookie, userAgent });
});
return new Response(JSON.stringify({ status: "success" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
```
The response is sent immediately while logging happens in the background.
**Common use cases:**
- Analytics tracking
- Audit logging
- Sending notifications
- Cache invalidation
- Cleanup tasks
**Important notes:**
- `after()` runs even if the response fails or redirects
- Works in Server Actions, Route Handlers, and Server Components
Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
@@ -0,0 +1,96 @@
---
title: Authenticate Server Actions Like API Routes
impact: CRITICAL
impactDescription: prevents unauthorized access to server mutations
tags: server, server-actions, authentication, security, authorization
---
## Authenticate Server Actions Like API Routes
**Impact: CRITICAL (prevents unauthorized access to server mutations)**
Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
**Incorrect (no authentication check):**
```typescript
"use server";
export async function deleteUser(userId: string) {
// Anyone can call this! No auth check
await db.user.delete({ where: { id: userId } });
return { success: true };
}
```
**Correct (authentication inside the action):**
```typescript
"use server";
import { verifySession } from "@/lib/auth";
import { unauthorized } from "@/lib/errors";
export async function deleteUser(userId: string) {
// Always check auth inside the action
const session = await verifySession();
if (!session) {
throw unauthorized("Must be logged in");
}
// Check authorization too
if (session.user.role !== "admin" && session.user.id !== userId) {
throw unauthorized("Cannot delete other users");
}
await db.user.delete({ where: { id: userId } });
return { success: true };
}
```
**With input validation:**
```typescript
"use server";
import { verifySession } from "@/lib/auth";
import { z } from "zod";
const updateProfileSchema = z.object({
userId: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
});
export async function updateProfile(data: unknown) {
// Validate input first
const validated = updateProfileSchema.parse(data);
// Then authenticate
const session = await verifySession();
if (!session) {
throw new Error("Unauthorized");
}
// Then authorize
if (session.user.id !== validated.userId) {
throw new Error("Can only update own profile");
}
// Finally perform the mutation
await db.user.update({
where: { id: validated.userId },
data: {
name: validated.name,
email: validated.email,
},
});
return { success: true };
}
```
Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
@@ -0,0 +1,41 @@
---
title: Cross-Request LRU Caching
impact: HIGH
impactDescription: caches across requests
tags: server, cache, lru, cross-request
---
## Cross-Request LRU Caching
`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
**Implementation:**
```typescript
import { LRUCache } from "lru-cache";
const cache = new LRUCache<string, any>({
max: 1000,
ttl: 5 * 60 * 1000, // 5 minutes
});
export async function getUser(id: string) {
const cached = cache.get(id);
if (cached) return cached;
const user = await db.user.findUnique({ where: { id } });
cache.set(id, user);
return user;
}
// Request 1: DB query, result cached
// Request 2: cache hit, no DB query
```
Use when sequential user actions hit multiple endpoints needing the same data within seconds.
**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
@@ -0,0 +1,76 @@
---
title: Per-Request Deduplication with React.cache()
impact: MEDIUM
impactDescription: deduplicates within request
tags: server, cache, react-cache, deduplication
---
## Per-Request Deduplication with React.cache()
Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
**Usage:**
```typescript
import { cache } from "react";
export const getCurrentUser = cache(async () => {
const session = await auth();
if (!session?.user?.id) return null;
return await db.user.findUnique({
where: { id: session.user.id },
});
});
```
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
**Avoid inline objects as arguments:**
`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
**Incorrect (always cache miss):**
```typescript
const getUser = cache(async (params: { uid: number }) => {
return await db.user.findUnique({ where: { id: params.uid } });
});
// Each call creates new object, never hits cache
getUser({ uid: 1 });
getUser({ uid: 1 }); // Cache miss, runs query again
```
**Correct (cache hit):**
```typescript
const getUser = cache(async (uid: number) => {
return await db.user.findUnique({ where: { id: uid } });
});
// Primitive args use value equality
getUser(1);
getUser(1); // Cache hit, returns cached result
```
If you must pass objects, pass the same reference:
```typescript
const params = { uid: 1 };
getUser(params); // Query runs
getUser(params); // Cache hit (same reference)
```
**Next.js-Specific Note:**
In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks:
- Database queries (Prisma, Drizzle, etc.)
- Heavy computations
- Authentication checks
- File system operations
- Any non-fetch async work
Use `React.cache()` to deduplicate these operations across your component tree.
Reference: [React.cache documentation](https://react.dev/reference/react/cache)
@@ -0,0 +1,65 @@
---
title: Avoid Duplicate Serialization in RSC Props
impact: LOW
impactDescription: reduces network payload by avoiding duplicate serialization
tags: server, rsc, serialization, props, client-components
---
## Avoid Duplicate Serialization in RSC Props
**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
**Incorrect (duplicates array):**
```tsx
// RSC: sends 6 strings (2 arrays × 3 items)
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
```
**Correct (sends 3 strings):**
```tsx
// RSC: send once
<ClientList usernames={usernames} />;
// Client: transform there
("use client");
const sorted = useMemo(() => [...usernames].sort(), [usernames]);
```
**Nested deduplication behavior:**
Deduplication works recursively. Impact varies by data type:
- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
```tsx
// string[] - duplicates everything
usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
// object[] - duplicates array structure only
users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
```
**Operations breaking deduplication (create new references):**
- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
**More examples:**
```tsx
// ❌ Bad
<C users={users} active={users.filter(u => u.active)} />
<C product={product} productName={product.name} />
// ✅ Good
<C users={users} />
<C product={product} />
// Do filtering/destructuring in client
```
**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
@@ -0,0 +1,145 @@
---
title: Hoist Static I/O to Module Level
impact: HIGH
impactDescription: avoids repeated file/network I/O per request
tags: server, io, performance, next.js, route-handlers, og-image
---
## Hoist Static I/O to Module Level
**Impact: HIGH (avoids repeated file/network I/O per request)**
When loading static assets (fonts, logos, images, config files) in route handlers or server functions, hoist the I/O operation to module level. Module-level code runs once when the module is first imported, not on every request. This eliminates redundant file system reads or network fetches that would otherwise run on every invocation.
**Incorrect (reads font file on every request):**
```typescript
// app/api/og/route.tsx
import { ImageResponse } from 'next/og'
export async function GET(request: Request) {
// Runs on EVERY request - expensive!
const fontData = await fetch(
new URL('./fonts/Inter.ttf', import.meta.url)
).then(res => res.arrayBuffer())
const logoData = await fetch(
new URL('./images/logo.png', import.meta.url)
).then(res => res.arrayBuffer())
return new ImageResponse(
<div style={{ fontFamily: 'Inter' }}>
<img src={logoData} />
Hello World
</div>,
{ fonts: [{ name: 'Inter', data: fontData }] }
)
}
```
**Correct (loads once at module initialization):**
```typescript
// app/api/og/route.tsx
import { ImageResponse } from 'next/og'
// Module-level: runs ONCE when module is first imported
const fontData = fetch(
new URL('./fonts/Inter.ttf', import.meta.url)
).then(res => res.arrayBuffer())
const logoData = fetch(
new URL('./images/logo.png', import.meta.url)
).then(res => res.arrayBuffer())
export async function GET(request: Request) {
// Await the already-started promises
const [font, logo] = await Promise.all([fontData, logoData])
return new ImageResponse(
<div style={{ fontFamily: 'Inter' }}>
<img src={logo} />
Hello World
</div>,
{ fonts: [{ name: 'Inter', data: font }] }
)
}
```
**Correct (synchronous fs at module level):**
```typescript
// app/api/og/route.tsx
import { ImageResponse } from 'next/og'
import { readFileSync } from 'fs'
import { join } from 'path'
// Synchronous read at module level - blocks only during module init
const fontData = readFileSync(
join(process.cwd(), 'public/fonts/Inter.ttf')
)
const logoData = readFileSync(
join(process.cwd(), 'public/images/logo.png')
)
export async function GET(request: Request) {
return new ImageResponse(
<div style={{ fontFamily: 'Inter' }}>
<img src={logoData} />
Hello World
</div>,
{ fonts: [{ name: 'Inter', data: fontData }] }
)
}
```
**Incorrect (reads config on every call):**
```typescript
import fs from "node:fs/promises";
export async function processRequest(data: Data) {
const config = JSON.parse(await fs.readFile("./config.json", "utf-8"));
const template = await fs.readFile("./template.html", "utf-8");
return render(template, data, config);
}
```
**Correct (hoists config and template to module level):**
```typescript
import fs from "node:fs/promises";
const configPromise = fs.readFile("./config.json", "utf-8").then(JSON.parse);
const templatePromise = fs.readFile("./template.html", "utf-8");
export async function processRequest(data: Data) {
const [config, template] = await Promise.all([
configPromise,
templatePromise,
]);
return render(template, data, config);
}
```
When to use this pattern:
- Loading fonts for OG image generation
- Loading static logos, icons, or watermarks
- Reading configuration files that don't change at runtime
- Loading email templates or other static templates
- Any static asset that's the same across all requests
When not to use this pattern:
- Assets that vary per request or user
- Files that may change during runtime (use caching with TTL instead)
- Large files that would consume too much memory if kept loaded
- Sensitive data that shouldn't persist in memory
With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute), module-level caching is especially effective because multiple concurrent requests share the same function instance. The static assets stay loaded in memory across requests without cold start penalties.
In traditional serverless, each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.
@@ -0,0 +1,83 @@
---
title: Parallel Data Fetching with Component Composition
impact: CRITICAL
impactDescription: eliminates server-side waterfalls
tags: server, rsc, parallel-fetching, composition
---
## Parallel Data Fetching with Component Composition
React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
**Incorrect (Sidebar waits for Page's fetch to complete):**
```tsx
export default async function Page() {
const header = await fetchHeader();
return (
<div>
<div>{header}</div>
<Sidebar />
</div>
);
}
async function Sidebar() {
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
```
**Correct (both fetch simultaneously):**
```tsx
async function Header() {
const data = await fetchHeader();
return <div>{data}</div>;
}
async function Sidebar() {
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
export default function Page() {
return (
<div>
<Header />
<Sidebar />
</div>
);
}
```
**Alternative with children prop:**
```tsx
async function Header() {
const data = await fetchHeader();
return <div>{data}</div>;
}
async function Sidebar() {
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
function Layout({ children }: { children: ReactNode }) {
return (
<div>
<Header />
{children}
</div>
);
}
export default function Page() {
return (
<Layout>
<Sidebar />
</Layout>
);
}
```
@@ -0,0 +1,32 @@
---
title: Parallel Nested Data Fetching
impact: CRITICAL
impactDescription: eliminates server-side waterfalls
tags: server, rsc, parallel-fetching, promise-chaining
---
## Parallel Nested Data Fetching
When fetching nested data in parallel, chain dependent fetches within each item's promise so a slow item doesn't block the rest.
**Incorrect (a single slow item blocks all nested fetches):**
```tsx
const chats = await Promise.all(chatIds.map((id) => getChat(id)));
const chatAuthors = await Promise.all(
chats.map((chat) => getUser(chat.author)),
);
```
If one `getChat(id)` out of 100 is extremely slow, the authors of the other 99 chats can't start loading even though their data is ready.
**Correct (each item chains its own nested fetch):**
```tsx
const chatAuthors = await Promise.all(
chatIds.map((id) => getChat(id).then((chat) => getUser(chat.author))),
);
```
Each item independently chains `getChat``getUser`, so a slow chat doesn't block author fetches for the others.
@@ -0,0 +1,38 @@
---
title: Minimize Serialization at RSC Boundaries
impact: HIGH
impactDescription: reduces data transfer size
tags: server, rsc, serialization, props
---
## Minimize Serialization at RSC Boundaries
The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
**Incorrect (serializes all 50 fields):**
```tsx
async function Page() {
const user = await fetchUser(); // 50 fields
return <Profile user={user} />;
}
("use client");
function Profile({ user }: { user: User }) {
return <div>{user.name}</div>; // uses 1 field
}
```
**Correct (serializes only 1 field):**
```tsx
async function Page() {
const user = await fetchUser();
return <Profile name={user.name} />;
}
("use client");
function Profile({ name }: { name: string }) {
return <div>{name}</div>;
}
```
-40
View File
@@ -1,40 +0,0 @@
acodec
adblocker
ahooks
antd
behaviour
bili
bilibili
bodys
cancle
cliqz
commitlint
Conpty
consola
conventionalcommits
datetime
DDTHH
execa
immer
inversify
KHTML
languagedetector
localforage
mediago
metas
nocheck
optimizelegibility
pinia
redownload
Redownload
reduxjs
Sider
stylelint
svgz
TDAPP
tiptap
typeorm
vuedraggable
vuejs
watting
Watting
+22
View File
@@ -0,0 +1,22 @@
**/node_modules
**/build
**/dist
**/release
**/.turbo
**/.git
**/.claude
**/.deps
*.md
# Go build artifacts
apps/core/bin/
apps/core/server
# Electron source (not needed, but keep package.json files for workspace/build)
apps/electron/src/
apps/electron/scripts/
docs/
# OS files
.DS_Store
**/.DS_Store
+15 -15
View File
@@ -1,15 +1,15 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false
+2 -3
View File
@@ -1,4 +1,3 @@
APP_NAME=mediago
APP_ID=mediago.ziying.site
APP_NAME=mediago-community
APP_ID=mediago.caorushizi.cn
APP_COPYRIGHT=caorushizi
APP_VERSION=2.2.2
+1 -2
View File
@@ -1,6 +1,5 @@
APP_TD_APPID=
APP_CLARITY_APPID=
APP_SENTRY_DSN=
GH_TOKEN=
LOAD_DEVTOOLS=
DEBUG_PLUGINS=true
+1 -2
View File
@@ -1,5 +1,4 @@
APP_TD_APPID=
APP_CLARITY_APPID=
APP_SENTRY_DSN=
GH_TOKEN=
DEBUG_PLUGINS=false
+3
View File
@@ -0,0 +1,3 @@
# Shell scripts MUST keep LF endings — CRLF from Windows checkouts breaks
# /bin/sh inside Linux containers (`/bin/sh^M: bad interpreter`).
*.sh text eol=lf
+58
View File
@@ -0,0 +1,58 @@
# GitHub Copilot Instruction: Code Review and Optimization
## Role and Objective
You are a **top-tier software architect and performance optimization expert**.
Your goal is to help me — a senior TypeScript full-stack engineer — elevate the quality of my code to a new level.
When reviewing my code, **do not explain basic concepts**. I need **precise, deep, and forward-thinking insights**.
Your core mission is **optimization**, including but not limited to:
- **Performance improvement**: Identify and optimize performance bottlenecks, reduce unnecessary computation and resource consumption.
- **Code refactoring**: Suggest more elegant and efficient implementations to improve code structure.
- **Design patterns**: Identify opportunities to apply or refine design patterns to enhance scalability and maintainability.
- **Best practices**: Ensure the code adheres to the latest best practices for the TypeScript ecosystem (Node.js, React, API layers, build pipelines).
- **Potential risks**: Anticipate and highlight deep issues such as concurrency problems, security vulnerabilities, or resource leaks.
---
## Review Perspective and Principles
1. **High-Standard Review**
Review the code as if it were going to **production for millions of users** and needs to be **maintained long-term**.
2. **Deep Analysis, Not Surface Advice**
Dont focus on trivial issues like typos or syntax sugar.
Instead, explain **why** a refactor or change matters — e.g.:
> “Switching from a synchronous file read to `fs.promises` can free the event loop, improving throughput under load.”
3. **Performance First**
- Evaluate time and space complexity; suggest algorithmic or structural improvements.
- Examine I/O, database queries, and network calls for efficiency — recommend batching, caching, or async processing.
- Recommend appropriate data structures or API strategies for scalability (e.g., pagination, streaming responses).
4. **Architecture and Design**
- Follow **SOLID** principles. Explicitly identify violations and propose refactoring approaches.
- Encourage **composition over inheritance** and **dependency injection**.
- Suggest modularization and clear separation between layers (e.g., API, service, repository, UI).
5. **Code Style and Standards**
- Code must be clear, consistent, and self-explanatory.
- Follow the projects existing conventions unless the change brings substantial clarity or performance gain.
- For complex logic, suggest adding comments explaining the **rationale (“why”)**, not just the **action (“what”)**.
---
## Specific Instructions
- **When reviewing code, include the optimized code snippet directly**, with short comments highlighting key changes and their reasoning.
- **If you find potential bugs or unhandled edge cases**, explicitly point them out and provide a fix suggestion.
- **Avoid subjective stylistic comments** unless they impact clarity, performance, or maintainability.
- **When I ask “Can this code be optimized?”**, provide a holistic evaluation covering performance, readability, scalability, and maintainability.
---
At the end of every response, please add:
> “AI-generated suggestions may contain errors; use your own judgment when applying them.”
@@ -1,7 +1,10 @@
name: Deploy Downloader Docs.
name: Deploy MediaGo Docs
on:
push:
paths:
- "docs/**"
- ".github/workflows/build-docs.yml"
branches: ["master"]
workflow_dispatch:
@@ -18,34 +21,39 @@ concurrency:
env:
BUCKET: downloader-docs
ENDPOINT: oss-cn-beijing.aliyuncs.com
ACCESS_KEY: LTAI5tLckcUrBtj7bCiUYwWz
ACCESS_KEY: LTAI5tDrRjFUAvufpCJioz3b
ACCESS_KEY_SECRET: ${{ secrets.ACCESS_KEY_SECRET }}
jobs:
build:
name: Build
name: Build Docs
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- uses: pnpm/action-setup@v5
with:
version: "10.15.0"
run_install: false
standalone: true
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 20
node-version: "24.14.0"
cache: "pnpm"
- uses: pnpm/action-setup@v3
with:
version: latest
run_install: true
- name: Install dependencies
run: pnpm install
- name: Build with Vitepress
run: pnpm run docs:build
run: pnpm docs:build
- name: Install Alibaba Cloud OSSUTIL
- name: Install Alibaba Cloud ossutil
run: wget http://gosspublic.alicdn.com/ossutil/1.6.10/ossutil64 && chmod +x ossutil64
- name: Configure Alibaba Cloud OSSUTIL
- name: Configure Alibaba Cloud ossutil
run: ./ossutil64 config -i ${ACCESS_KEY} -k ${ACCESS_KEY_SECRET} -e ${ENDPOINT} -c .ossutilconfig
- name: Upload the web folder to the chosen OSS bucket
+72
View File
@@ -0,0 +1,72 @@
# build.yml
# Workflow's name
name: Build MediaGo App
# Workflow's trigger
on:
workflow_dispatch:
# Workflow's jobs
jobs:
# job's id
release:
# job's name
name: build and release electron app
# the type of machine to run the job on
runs-on: ${{ matrix.os }}
# create a build matrix for jobs
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest, macos-15-intel, ubuntu-latest]
# create steps
steps:
- name: Check out git repository
uses: actions/checkout@v6
- uses: pnpm/action-setup@v5
with:
version: "10.15.0"
run_install: false
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: "24.14.0"
cache: "pnpm"
- name: Install Python
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: "1.25.0"
cache-dependency-path: |
apps/core/go.sum
- name: Install swag
run: go install github.com/swaggo/swag/cmd/swag@latest
- name: Install dependencies
run: pnpm install
- name: Download third-party deps
run: pnpm deps:download
- name: Build & release app
run: pnpm release:electron
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
APP_TD_APPID: ${{ secrets.APP_TD_APPID }}
- uses: actions/upload-artifact@v6
with:
name: mediago-${{ matrix.os }}
path: apps/electron/release/mediago-*
+153
View File
@@ -0,0 +1,153 @@
name: Build & Push Docker Image
on:
workflow_dispatch:
inputs:
tag:
description: "Image tag to publish under (e.g. 3.5.0 to rebuild a released version, or 3.5.0-fix.1 for a side-by-side). Leave blank for dev-<run_id>."
required: false
type: string
push_latest:
description: "Also tag the resulting image as `latest` (use when rebuilding a stable release)."
required: false
type: boolean
default: false
push:
tags:
- "v*"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# Docker Hub mirror — same image, different registry. Credentials
# come from the DOCKERHUB_USERNAME / DOCKERHUB_TOKEN secrets (token,
# not account password). If either secret is empty, the Docker Hub
# login step is skipped and only GHCR gets pushed.
DOCKERHUB_IMAGE: caorushizi/mediago
jobs:
docker:
name: Build and push multi-arch image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# GitHub Actions forbids `secrets.*` inside `if:` expressions
# directly — it fails with `Unrecognized named-value: 'secrets'`.
# The blessed workaround: pull the secret through `env:` (which
# IS allowed) inside a single check step, write a boolean to
# step outputs, and have every downstream step guard on the
# output instead of the raw secret.
- name: Detect Docker Hub credentials
id: dockerhub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
if [ -n "$DOCKERHUB_USERNAME" ] && [ -n "$DOCKERHUB_TOKEN" ]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
fi
# Only log in (and later push) to Docker Hub when both secrets are
# configured. Lets forks / trial runs without Docker Hub
# credentials still build-and-push to GHCR.
- name: Login to Docker Hub
if: steps.dockerhub.outputs.enabled == 'true'
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Compose the list of target images based on which registries we
# actually have credentials for. Without this, metadata-action
# would always generate Docker Hub tags and build-push-action
# would then fail with 401 on forks that haven't set the secrets.
- name: Resolve image targets
id: targets
run: |
{
echo "images<<EOF"
echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
if [ "${{ steps.dockerhub.outputs.enabled }}" = "true" ]; then
echo "${{ env.DOCKERHUB_IMAGE }}"
fi
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ steps.targets.outputs.images }}
tags: |
# 1. Manual rebuild with an explicit tag (e.g. `3.5.0`, `3.5.0-fix.1`)
type=raw,value=${{ inputs.tag }},enable=${{ inputs.tag != '' }}
# 2. Tag push: v1.0.0 → 1.0.0
type=semver,pattern={{version}}
# 3. `latest`: on stable semver tag pushes, or when the manual
# rebuild explicitly asks for it
type=raw,value=latest,enable=${{ (startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'beta')) || inputs.push_latest }}
# 4. Fallback for manual triggers with no custom tag
type=raw,value=dev-${{ github.run_id }},enable=${{ !startsWith(github.ref, 'refs/tags/') && inputs.tag == '' }}
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
# `metadata-action` already emits the standard OCI labels
# (image.title / description / source / licenses / version /
# revision / created). `revision` uses github.sha, so two
# builds that re-use a tag are distinguishable via
# `docker inspect`.
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build Summary
run: |
echo "### 🎉 Docker Image Published" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Registries:**" >> $GITHUB_STEP_SUMMARY
echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\`" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.dockerhub.outputs.enabled }}" = "true" ]; then
echo "- \`docker.io/${{ env.DOCKERHUB_IMAGE }}\`" >> $GITHUB_STEP_SUMMARY
fi
echo "**Platforms:** linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY
echo "**Version:** \`${{ steps.meta.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Tags:**" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.meta.outputs.tags }}" | while read tag; do
echo "- \`${tag}\`" >> $GITHUB_STEP_SUMMARY
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Pull:**" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.dockerhub.outputs.enabled }}" = "true" ]; then
echo "docker pull ${{ env.DOCKERHUB_IMAGE }}:${{ steps.meta.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
-63
View File
@@ -1,63 +0,0 @@
# build.yml
# Workflow's name
name: Build Downloader App.
# Workflow's trigger
on:
push:
branches:
- master
# Workflow's jobs
jobs:
# job's id
release:
# job's name
name: build and release electron app
# the type of machine to run the job on
runs-on: ${{ matrix.os }}
# create a build matrix for jobs
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
# create steps
steps:
# step1: check out repository
- name: Check out git repository
uses: actions/checkout@v4
# step2: install node env
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
# step4: install python env
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
# step5: install pnpm env
- uses: pnpm/action-setup@v3
with:
version: latest
run_install: true
standalone: true
- name: Build & release app
run: pnpm run release
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
APP_TD_APPID: ${{ secrets.APP_TD_APPID }}
APP_CLARITY_APPID: ${{ secrets.APP_CLARITY_APPID}}
- uses: actions/upload-artifact@v4
with:
name: mediago-${{ matrix.os }}
path: packages/main/release/mediago-setup-*
+17 -1
View File
@@ -2,5 +2,21 @@ node_modules
*.local
.idea
.parcel-cache
.vitepress
*.tsno.mjs
.DS_Store
log
.turbo
.claude
.store
release
go.work.sum
apps/core/bin/
apps/electron/bin/
apps/main/bin/
.deps/
tmp/
# Local planning notes that should not be published with the open-source repo.
docs/.vitepress/seo-keyword-map*.md
docs/.vitepress/private/
-13
View File
@@ -1,13 +0,0 @@
{
"extends": [
"development"
],
"hints": {
"axe/text-alternatives": [
"default",
{
"image-alt": "off"
}
]
}
}
-5
View File
@@ -1,5 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit ${1}
npm run commitlint ${1}
+1 -4
View File
@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
npx tsx ./scripts/pre-commit.ts
lint-staged
-3
View File
@@ -1,3 +0,0 @@
registry=https://registry.npmmirror.com
electron_mirror=https://npmmirror.com/mirrors/electron/
package-manager=pnpm
+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 80,
"indentStyle": "space",
"indentWidth": 2,
"endOfLine": "lf",
"sortPackageJson": false,
"ignorePatterns": [
"build",
"dist",
"release",
"coverage",
"docs/.vitepress/cache",
"docs/.vitepress/dist"
],
"overrides": [
{
"files": ["*.md"],
"options": {
"proseWrap": "preserve"
}
}
]
}
+53
View File
@@ -0,0 +1,53 @@
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
"categories": {
"correctness": "error",
"suspicious": "warn",
"perf": "warn",
"pedantic": "off",
"style": "off",
"restriction": "off",
"nursery": "off"
},
"rules": {
"no-unused-vars": "error",
"no-console": "warn",
"no-debugger": "error",
"no-duplicate-imports": "error",
"eqeqeq": "warn",
"typescript/no-explicit-any": "warn",
"typescript/no-non-null-assertion": "warn",
"import/no-duplicates": "error",
"import/no-self-import": "error",
"unicorn/no-null": "off",
"unicorn/prefer-node-protocol": "error"
},
"overrides": [
{
"files": ["**/*.tsx", "**/*.jsx"],
"rules": {
"react/jsx-no-duplicate-props": "error",
"react/no-direct-mutation-state": "error",
"react/jsx-key": "error",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
},
{
"files": ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts"],
"rules": {
"no-console": "off",
"typescript/no-explicit-any": "off"
}
}
],
"ignorePatterns": [
"node_modules",
"build",
"dist",
"release",
"*.min.js",
"docs/.vitepress/cache",
"docs/.vitepress/dist"
]
}
-3
View File
@@ -1,3 +0,0 @@
# Ignore artifacts:
build
coverage
-1
View File
@@ -1 +0,0 @@
{}
+4
View File
@@ -0,0 +1,4 @@
{
"recommendations": ["oxc.oxc-vscode", "EditorConfig.EditorConfig"],
"unwantedRecommendations": ["esbenp.prettier-vscode"]
}
+15
View File
@@ -0,0 +1,15 @@
{
"cSpell.words": ["intlify", "unplugin", "vitepress", "waline"],
"oxc.enable": true,
"oxc.lint.enable": true,
"oxc.fmt.experimental": true,
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.oxc": "explicit"
},
"prettier.enable": false,
"[go]": {
"editor.defaultFormatter": "golang.go"
}
}
+21
View File
@@ -0,0 +1,21 @@
# Repository Guidelines
## Project Structure & Module Organization
MediaGo is a pnpm/turborepo monorepo. Feature apps live in `apps/` (`frontend-main`, `frontend-mobile`, `backend-web`, `backend-electron`) for the user surfaces and API. Reusable logic stays in `packages/` (`shared` for cross-runtime helpers, `backend` for orchestration, `main` for Electron packaging). Long-form docs and assets sit in `docs/`, `images/`, and `docker/`. End-to-end checks live in `tests/`.
## Build, Test, and Development Commands
Run `pnpm install` once per clone. Use `pnpm dev` for the unified desktop + web experience, or scope to `pnpm dev:web` / `pnpm dev:electron`. `pnpm build` triggers the production Turborepo pipeline; `pnpm build:web-release` plus `pnpm build:docker` produce the deployable web bundle. Keep the codebase healthy with `pnpm lint`, `pnpm lint:fix`, `pnpm format`, and verify types through `pnpm types`.
## Coding Style & Naming Conventions
Target modern TypeScript with ES modules, two-space indentation, UTF-8, and LF endings per `.editorconfig`. Components, hooks, and services adopt PascalCase (e.g. `UserPreferencesPanel.tsx`). Utilities and helpers stay camelCase, and constants use SCREAMING_SNAKE_CASE. Always run `pnpm format` before committing; reserve comments for clarifying complex logic.
## Testing Guidelines
Integration suites live under `tests/*.test.ts` and execute via `pnpm test` using the Node `tsx` runner. Name files descriptively like `download.queue.integration.test.ts`. Mock external services, prefer shared fixtures in `tests/fixtures/`, and cover happy path, recovery, and edge behaviors when touching runtime code.
## Commit & Pull Request Guidelines
Follow Conventional Commits (e.g. `feat(frontend-main): add download queue UI`) and use `pnpm commit` (Commitizen) to stay compliant. Pull requests should summarize the change, link issues with `Closes #123`, note local test runs (`pnpm test`), and attach screenshots or recordings for UI updates. Call out new environment variables, migrations, or follow-up tasks so reviewers can reproduce the setup quickly.
+93
View File
@@ -0,0 +1,93 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
MediaGo is a cross-platform video downloader supporting m3u8/HLS streams. The codebase is a pnpm monorepo with three products:
1. **Desktop app** (`apps/electron` + `apps/ui`) — Electron wrapper that launches Go Core as a subprocess
2. **Web server** (`apps/server` + `apps/ui`) — Node.js launcher that spawns Go Core as a subprocess
3. **Video player** (`apps/core` + `apps/player-ui`) — Player UI embedded in Go Core for video playback
All three products share the Go Core backend (`apps/core`) for download orchestration.
## Common Commands
```bash
pnpm install # Install all dependencies (run once per clone)
pnpm dev:electron # Start Electron desktop dev environment (HMR)
pnpm dev:server # Start server dev environment (HMR)
pnpm build:electron # Production build for Electron
pnpm build:web # Build UI only (server mode)
pnpm core:dev # Start Go Core dev server (port 9900)
pnpm core:build # Compile Go Core binary
pnpm player:dev # Start Player dev (alias for core:dev)
pnpm player:build # Build Player (alias for core:build)
pnpm deps:download # Download third-party tools (ffmpeg, BBDown, etc.)
pnpm deps:download:all # Download tools for all platforms
pnpm lint # Lint with oxlint
pnpm lint:fix # Auto-fix lint issues
pnpm format # Format with oxfmt
pnpm format:check # Check formatting without modifying
pnpm check # Full check: lint + format + type check
pnpm type:check # TypeScript type checking via Turborepo
pnpm pack:electron # Build + package Electron distributable
```
Commits use Conventional Commits format (e.g. `feat(electron): add queue UI`).
## Architecture
### Monorepo Layout
**Apps:**
- **`apps/core/`** — Go (Gin) REST API backend for download orchestration. Runs on port 9900. Uses SQLite (GORM), SSE for real-time events, PTY for capturing download tool output. Built with Gulp + Go cross-compilation.
- **`apps/electron/`** — Electron main process (tsdown build, inversify DI). Launches Go Core via `@mediago/service-runner`.
- **`apps/server/`** — Node.js launcher (tsdown build). Spawns Go Core via `@mediago/service-runner`.
- **`apps/ui/`** — Shared React 19 frontend (Vite 8, Ant Design 6, Zustand, TailwindCSS 4, i18next). Used by both Electron and server targets.
- **`apps/player-ui/`** — React 19 frontend for player (Vite 8, shadcn/ui, video.js, TailwindCSS 4). Built assets are embedded into Go Core via `//go:embed`.
**Packages:**
- **`packages/shared/common/`** — Platform-agnostic shared types, constants, and utilities
- **`packages/core-sdk/`** — TypeScript SDK for Go Core REST API (Axios, SSE via eventsource)
- **`packages/electron-preload/`** — Electron preload scripts for IPC bridge
- **`packages/browser-extension/`** — Browser extension (Lit web components)
- **`docs/`** — VitePress documentation (Chinese, English, Japanese)
### Multi-Target Build
The `APP_TARGET` env var (`electron` | `server`) controls which backend the UI builds against. Both targets share the same React UI but connect via different transports:
- **Electron**: IPC bridge (preload) + Go Core direct (via `@mediago/core-sdk`)
- **Server/Web**: HTTP/WebSocket + Go Core direct (via `@mediago/core-sdk`)
The UI adapter layer (`apps/ui/src/hooks/adapters/`) abstracts this: `electron.ts` provides IPC bridge in desktop mode, `platform-stubs.ts` provides no-op stubs in web mode, and `index.ts` exports `platformApi` which selects the appropriate adapter.
### Key Patterns
- **Go Core as subprocess**: Both Electron and server apps launch Go Core via `@mediago/service-runner`, which manages the process lifecycle and port allocation
- **Dependency Injection**: inversify with `@inversifyjs/binding-decorators` in Electron backend
- **State Management**: Zustand in the UI
- **Real-time events**: Go Core emits SSE events (`/api/events`); the UI's `api/events.ts` subscribes and dispatches to React via a listener pattern
- **TypeScript**: Strict mode with experimental decorators and decorator metadata enabled
- **Module format**: ES Modules everywhere
## Tooling
- **Package manager**: pnpm 10.15.0 (enforced via `packageManager` field)
- **Build orchestration**: Turborepo
- **App bundling**: tsdown for Node/Electron, Vite 8 for UI apps
- **Go builds**: Gulp orchestrating `go build` / `go run` in `apps/core`
- **Linter**: oxlint (config in `.oxlintrc.json`)
- **Formatter**: oxfmt (config in `.oxfmtrc.json`)
- **Pre-commit**: husky + lint-staged (runs oxlint --fix + oxfmt --write on staged files)
- **Electron packaging**: electron-builder
## Style Conventions
- TypeScript, ES modules, 2-space indentation, UTF-8, LF endings
- Components: PascalCase. Utilities: camelCase. Constants: SCREAMING_SNAKE_CASE
- UI port: 8555 (strict). Go Core port: 9900. Player UI port: 8556
+96
View File
@@ -0,0 +1,96 @@
# Contributing to MediaGo
Thanks for your interest in hacking on MediaGo! This doc covers everything
you need to get a local dev build running. For user-facing usage, see the
main [README](./README.md).
## Prerequisites
- **Node.js** ≥ 20 — install from [nodejs.org](https://nodejs.org/)
- **pnpm** ≥ 10 — `npm i -g pnpm`
- **Go** ≥ 1.22 — only needed if you're working on the Go Core backend
(`apps/core/`). Install from [go.dev](https://go.dev/dl/).
## Clone & install
```shell
git clone https://github.com/caorushizi/mediago.git
cd mediago
pnpm install
```
## Repository layout
MediaGo is a pnpm + Turborepo monorepo with three products that share the
same Go Core backend:
```
apps/
core/ Go backend (download orchestration, SSE, REST API)
electron/ Electron desktop main process
server/ Node.js launcher for the self-hosted web build
ui/ Shared React 19 frontend (Electron + Web)
player-ui/ React frontend embedded inside Go Core for playback
packages/
shared/common/ Cross-platform types, constants, i18n resources
core-sdk/ TypeScript SDK for the Go Core REST API
electron-preload/
mediago-extension/ Browser extension (Chrome / Edge)
docs/ VitePress site (zh / en / jp)
extra/ Vendored binaries (e.g. aria2)
scripts/ Dep downloaders, extension packager, etc.
```
Deeper architecture notes live in [`CLAUDE.md`](./CLAUDE.md).
## Everyday commands
```shell
# Download third-party binaries (ffmpeg, yt-dlp, N_m3u8DL-RE, BBDown,
# aria2, mediago-core) for the current platform — run once per clone
pnpm deps:download
# Run the Electron desktop app in dev mode (HMR)
pnpm dev:electron
# Run the self-hosted web server in dev mode
pnpm dev:server
# Build an unpacked Electron directory (fast, for smoke-testing layout)
pnpm pack:electron
# Build full Electron installers for distribution (.exe / .dmg / .deb)
pnpm release:electron
# Lint + format + type-check (what CI runs)
pnpm check
```
The self-hosted web server doesn't have a dedicated packaging script — it
ships via the Docker image published to GHCR, or you can run the build
output (`pnpm -F @mediago/server build`) directly under Node.
## Commit style
This repo uses [Conventional Commits](https://www.conventionalcommits.org/).
Typical shapes:
```
feat(ui): add dark-mode toggle to settings page
fix(core): resume m3u8 downloads after process restart
refactor(extension): split options hook per card
chore(deps): bump axios from 1.14.0 to 1.15.0
```
Commits are lint-staged on commit (oxlint --fix + oxfmt --write on
staged files). Type checks run via `turbo type:check`.
## Pull requests
- Open PRs against the `master` branch.
- Keep each PR focused — one feature / fix per PR if possible.
- Include a short "why" in the description; the "what" is in the diff.
- If the change is user-visible, a line in the PR description that would
fit in a release note is appreciated.
Thanks for contributing! 🚀
+117
View File
@@ -0,0 +1,117 @@
# Build context: repository root
# docker build -t mediago .
# ===== Stage 1: Node Builder (runs natively on build machine) =====
FROM --platform=$BUILDPLATFORM node:22-bookworm-slim AS node-builder
RUN apt-get update && apt-get install -y --no-install-recommends unzip && rm -rf /var/lib/apt/lists/*
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
# Map Docker TARGETARCH to Node arch naming for deps download
ARG TARGETARCH
RUN if [ "$TARGETARCH" = "amd64" ]; then \
echo "linux-x64" > /tmp/deps-platform; \
else \
echo "linux-${TARGETARCH}" > /tmp/deps-platform; \
fi
WORKDIR /src
# Install dependencies — copy all workspace package.json files first for caching
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc* ./
COPY apps/ui/package.json apps/ui/package.json
COPY apps/player-ui/package.json apps/player-ui/package.json
COPY apps/server/package.json apps/server/package.json
COPY apps/core/package.json apps/core/package.json
COPY apps/electron/package.json apps/electron/package.json
COPY packages/ packages/
RUN pnpm install --frozen-lockfile
# Copy source files and root configs needed by builds
COPY tsconfig*.json turbo.json .env* ./
COPY apps/ui/ apps/ui/
COPY apps/player-ui/ apps/player-ui/
COPY apps/electron/app/package.json apps/electron/app/package.json
COPY scripts/ scripts/
# Vendored binaries (aria2 in particular). `scripts/download-deps.ts`
# treats `source: "local"` entries by copying from `extra/<tool>/<os>/<arch>/`
# into `.deps/`; without this COPY they silently get skipped and the
# resulting image ships with no aria2c.
COPY extra/ extra/
# Build player-ui (will be embedded in Go core binary)
RUN pnpm --filter @mediago/player-ui run build
# Build web UI for server mode
ENV APP_TARGET=server
ENV NODE_ENV=production
RUN pnpm build:web
# Download third-party tools for TARGET platform
RUN pnpm deps:download --platform $(cat /tmp/deps-platform)
# ===== Stage 2: Go Builder =====
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS go-builder
ARG TARGETOS=linux
ARG TARGETARCH=amd64
WORKDIR /src
# Cache Go module downloads
COPY apps/core/go.mod apps/core/go.sum apps/core/
RUN cd apps/core && go mod download
# Copy Go source
COPY apps/core/ apps/core/
# Copy player-ui dist into assets for go:embed
COPY --from=node-builder /src/apps/player-ui/dist apps/core/assets/player/
# Ensure all dependencies are downloaded (go.sum may have been updated)
RUN cd apps/core && go mod download
# Build Go core binary (cross-compile, no CGO)
RUN cd apps/core && \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -trimpath -ldflags="-s -w" -o /out/mediago-core ./cmd/server
# ===== Stage 3: Runtime =====
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates libicu72 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Core binary
COPY --from=go-builder /out/mediago-core /usr/local/bin/mediago-core
# Web UI static files
COPY --from=node-builder /src/apps/ui/build/server /app/static
# Third-party tools — copy from the platform-specific directory
ARG TARGETARCH
COPY --from=node-builder /src/.deps/linux-* /app/deps-tmp/
RUN mkdir -p /app/deps && \
if [ -d /app/deps-tmp ]; then \
find /app/deps-tmp -type f -exec cp {} /app/deps/ \; ; \
fi && \
chmod +x /app/deps/* 2>/dev/null || true && \
rm -rf /app/deps-tmp
RUN mkdir -p /app/mediago/data /app/mediago/logs /app/mediago/downloads
# Entrypoint script — isolates the invocation flags from the Dockerfile
# so editing the default args doesn't require rebuilding the full image
# layer and so callers can still append overrides via `docker run`.
COPY scripts/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
EXPOSE 8899
VOLUME ["/app/mediago"]
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
+177
View File
@@ -0,0 +1,177 @@
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/it/guides.html?form=github">Avvio rapido</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/it?form=github">Sito web</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/it/documents.html?form=github">Documentazione</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/discussions">Discussioni</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://discord.gg/yxWBVRWGqM">Discord</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://www.reddit.com/r/MediaGo_Studio/">Reddit</a>
<br>
<a href="https://github.com/caorushizi/mediago/blob/master/README.md">English</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/blob/master/README.zh.md">中文</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/blob/master/README.jp.md">日本語</a>
<br>
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
<img alt="GitCode" src="https://gitcode.com/caorushizi/mediago/star/badge.svg">
<br>
<a href="https://trendshift.io/repositories/11083" target="_blank">
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
</a>
<hr />
</div>
Un downloader video multipiattaforma con sniffing integrato: apri una
pagina, scegli la risorsa che ti interessa e salvala. Nessuna cattura dei
pacchetti, nessuna configurazione complicata di estensioni browser, nessuno
strumento da riga di comando da gestire.
L'interfaccia dell'app include attualmente inglese, cinese semplificato e
italiano.
## ✨ Cosa include
### 🌐 Estensione browser per Chrome / Edge
Trovi un video interessante su un sito qualsiasi → clicchi l'estensione →
lo invii a MediaGo con un clic. Rileva automaticamente le risorse video,
mostra il numero di elementi trovati nel badge della toolbar e funziona con
le principali piattaforme video, incluse YouTube, Bilibili e molte altre.
L'estensione è inclusa nell'app desktop: apri **Impostazioni → Altre
impostazioni → Directory estensione browser** per trovare la cartella di
installazione.
### 🎬 YouTube e oltre 1000 siti
Basato su yt-dlp. Supporta YouTube, Twitter/X, Instagram, Reddit e
[oltre mille altri siti video](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md).
### 🦞 Gli assistenti AI possono scaricare per te — OpenClaw Skill
Usi Claude Code, Cursor o un altro assistente AI per programmare? Installa
la skill MediaGo e scrivi semplicemente _"please download this video:
&lt;url&gt;"_. L'assistente gestisce il resto.
```shell
npx clawhub@latest install mediago
```
### 🔌 Funziona con altri strumenti
MediaGo espone una API HTTP completa: script, automazioni e app di terze
parti possono creare attività di download, consultare l'avanzamento e
gestire la lista. L'estensione browser usa la stessa API per parlare con
l'app desktop, e puoi integrarla anche nei tuoi workflow.
### 🎞️ Conversione formato integrata
Dopo il download puoi convertire il file in un altro formato o qualità
direttamente da MediaGo. Non serve aprire uno strumento ffmpeg separato.
### 🐳 Deploy Docker con un solo comando
Installazione headless sul tuo server, poi accesso alla UI web da qualsiasi
dispositivo nella stessa rete:
```shell
docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago caorushizi/mediago:3.5.0
```
Disponibile su [Docker Hub](https://hub.docker.com/r/caorushizi/mediago)
e GHCR (`ghcr.io/caorushizi/mediago`): la stessa immagine, scegli il
registry più veloce per te. Supporta Intel / AMD (amd64) e ARM (arm64).
Nella build desktop, MediaGo ascolta sia su `127.0.0.1` sia sull'IP LAN,
così telefoni e tablet sulla stessa rete Wi-Fi possono aprire direttamente
la UI web.
## 📷 Screenshot
![Home](./images/home_en.png)
![Home — modalità scura](./images/home-dark_en.png)
![Impostazioni](./images/settings_en.png)
![Estrazione risorse](./images/browser_en.png)
## 📥 Download
### v3.5.0 (stabile)
- [Windows — installer](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-win32-x64-3.5.0.exe)
- [Windows — portable](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-portable-win32-x64-3.5.0.exe)
- [macOS — Apple Silicon (arm64)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-darwin-arm64-3.5.0.dmg)
- [macOS — Intel (x64)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-darwin-x64-3.5.0.dmg)
- [Linux (deb)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-linux-amd64-3.5.0.deb)
- [**Docker Hub**](https://hub.docker.com/r/caorushizi/mediago): `docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago caorushizi/mediago:3.5.0`
- **GHCR**: `docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago ghcr.io/caorushizi/mediago:3.5.0`
Per le versioni precedenti, consulta la [pagina GitHub Releases](https://github.com/caorushizi/mediago/releases).
### 🪄 Deploy Docker con un clic tramite BT Panel
1. Installa [BT Panel](https://www.bt.cn/new/download.html?r=dk_mediago)
usando lo script ufficiale.
2. Accedi al pannello, clicca **Docker** nella barra laterale e completa la
configurazione del servizio Docker seguendo le istruzioni.
3. Trova **MediaGo** nello store delle app, clicca **Install**, configura il
dominio e hai finito.
## 📝 Novità in v3.5.0
- **🌐 Estensione browser** — sniffing video su qualsiasi sito e invio a
MediaGo con un clic
- **🎬 YouTube + 1000+ siti** — integrazione con yt-dlp
- **🦞 OpenClaw Skill** — scarica video tramite assistenti AI per programmare
- **🔌 API HTTP** — integrazione con script, automazioni e strumenti di terze parti
- **🎞️ Conversione formato in app** — scegli formato e qualità di output
- **🐳 Deploy Docker più semplice** — monta una sola cartella, immagini multi-arch su GHCR
- **⚡ Avvio più rapido** — backend riscritto, minore consumo di memoria, player video integrato
## 🛠️ Tecnologie
[![React](https://img.shields.io/badge/React-20232A?logo=react&logoColor=61DAFB)](https://react.dev/)
[![Electron](https://img.shields.io/badge/Electron-191970?logo=electron&logoColor=white)](https://www.electronjs.org)
[![Vite](https://img.shields.io/badge/Vite-646CFF?logo=vite&logoColor=white)](https://vitejs.dev)
[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-06B6D4?logo=tailwindcss&logoColor=white)](https://tailwindcss.com)
[![shadcn/ui](https://img.shields.io/badge/shadcn%2Fui-000?logo=shadcnui&logoColor=white)](https://ui.shadcn.com/)
[![Go](https://img.shields.io/badge/Go-00ADD8?logo=go&logoColor=white)](https://go.dev/)
[![Ant Design](https://img.shields.io/badge/Ant_Design-0170FE?logo=antdesign&logoColor=white)](https://ant.design)
## 🙏 Ringraziamenti
- [N_m3u8DL-RE](https://github.com/nilaoda/N_m3u8DL-RE)
- [BBDown](https://github.com/nilaoda/BBDown)
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- [aria2](https://aria2.github.io/)
- [mediago-core](https://github.com/caorushizi/mediago-core)
## ⚖️ Disclaimer
> **Questo progetto è destinato esclusivamente a scopi educativi e di ricerca. Non usarlo per finalità commerciali o illegali.**
>
> 1. Tutto il codice e tutte le funzionalità fornite da questo progetto sono pensati solo come riferimento per lo studio delle tecnologie di streaming. Gli utenti devono rispettare le leggi e i regolamenti della propria giurisdizione.
> 2. Qualsiasi contenuto scaricato tramite questo progetto resta di proprietà dei rispettivi titolari dei diritti. Gli utenti devono eliminare i contenuti scaricati entro 24 ore o ottenere un'autorizzazione adeguata.
> 3. Gli sviluppatori del progetto non sono responsabili delle azioni degli utenti, incluso il download di contenuti protetti da copyright o l'impatto su piattaforme di terze parti.
> 4. È vietato usare questo progetto per scraping massivo, interruzione dei servizi delle piattaforme o qualsiasi attività che violi diritti legittimi altrui.
> 5. Usando questo progetto confermi di aver letto e accettato questo disclaimer. Se non lo accetti, interrompi subito l'uso del progetto ed eliminalo.
---
> Vuoi compilare da sorgente? Consulta [CONTRIBUTING.md](./CONTRIBUTING.md).
>
> Vuoi tradurre MediaGo? Consulta [TRANSLATION.md](./TRANSLATION.md).

Some files were not shown because too many files have changed in this diff Show More