mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(react-doctor): correct test exclusion + React-Compiler lint policy + state-sync fix (#1155)
* fix(react-doctor): correctly exclude test files from scoring The intended test-file ignore in react-doctor.config.json was never applied: react-doctor's config precedence reads the "reactDoctor" key in package.json (which had no ignore), shadowing the config file. On top of that, react-doctor 0.4.0's ignore.files matcher is broken — any non-empty value collapses scan scope and drops real product files, not just tests. Consolidate to a single canonical doctor.config.json using ignore.overrides (which works correctly): only test files are excluded while all product code is still scored. Remove the shadowing package.json key and the dead react-doctor.config.json. Product-code baseline is 55 (92 errors, 515 warnings, 118 files). * chore(react-doctor): add long-running task tracking for score effort * refactor(react): remove compiler-redundant memoization in verified files Delete manual useMemo/useCallback/memo that the React Compiler already handles, in 7 files validated to be behavior-preserving (factories are pure functions of compiler-trackable reactive inputs). Kept memos whose factories read external mutable DOM/theme state with load-bearing deps (e.g. use-reply-height-estimates metrics). Also hoists a regex and reads a localStorage value once. Note: this is code-quality cleanup; react-doctor's score is error- weighted, so warning cleanup like this does not move the score. See docs/agent-runs/react-doctor-score/progress.md. * fix(react-doctor): adopt React-Compiler lint policy + fix one state-sync bug react-doctor's score is dominated by React-Compiler optimizability diagnostics that flag intentional patterns (the latest-ref idiom) and current compiler limitations (try/finally, throw-in-try/catch the compiler can't lower yet), not bugs. Rewriting that working code to satisfy them would degrade it. - Replace doctor.config.json with a documented doctor.config.jsonc that does not enforce the react-hooks-js (React Compiler) rules or react-compiler-no-manual-memoization. All real code-quality, a11y, and performance rules stay enforced. - Fix one genuine state-sync bug: use-now-seconds refreshed 'now' via a synchronous setState inside an effect (an extra render with a stale value); move it to a render-time prev-prop comparison (React's adjust-during-render pattern), behavior-equivalent. Score 54 (broken config) -> 63. type-check/lint/1051 tests pass; browser smoke confirms timestamps render with no re-render regression. The remaining no-adjust-state-on-prop-change diagnostics are real bugs but entangled with legitimate side effects (navigate/ref-cancel/async) in critical flows; left for careful follow-up. * chore(react-doctor): remove the vanity score badge, keep PR-diff review The single 0-100 react-doctor score mostly reflects React-Compiler optimizability and isn't a meaningful health grade to display (see docs/agent-runs/react-doctor-score). Remove the README badge and its now- dead generation infra (CI write/upload/publish steps + the write-react-doctor-badge.mjs script + doctor:badge package script). Kept: react-doctor's actual value -- the PR step that runs 'yarn doctor --diff <base> --annotations' on pull requests touching React files, surfacing newly-introduced issues inline. Coverage badge untouched. * docs(react-doctor): document why the score is not a target to chase Record the reasoning so future agents/contributors don't re-attempt to grind the react-doctor score: it overwhelmingly reflects React-Compiler optimizability (most 'errors' flag intentional patterns and current compiler limitations, not bugs) and saturates on the fraction of clean files, so ~63 is the honest ceiling and 90 only comes from disabling the linter. - Add a known-surprises entry with the full reasoning + mitigation. - Reframe the AGENTS.md react-doctor verification line: it's a PR-diff reviewer for newly-introduced issues, not an aggregate score to raise.
This commit is contained in:
@@ -100,18 +100,6 @@ jobs:
|
||||
if: github.event_name == 'pull_request' && steps.react-ui-changes.outputs.changed != 'true'
|
||||
run: echo "Skipping React Doctor because this pull request did not change React UI source."
|
||||
|
||||
- name: Write React Doctor badge payload
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
run: node scripts/write-react-doctor-badge.mjs
|
||||
|
||||
- name: Upload React Doctor badge
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: react-doctor-badge
|
||||
path: badges/react-doctor.json
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Install Chromium for smoke tests
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
@@ -148,21 +136,13 @@ jobs:
|
||||
name: coverage-badge
|
||||
path: ${{ runner.temp }}/coverage-badge
|
||||
|
||||
- name: Download React Doctor badge
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: react-doctor-badge
|
||||
path: ${{ runner.temp }}/react-doctor-badge
|
||||
|
||||
- name: Prepare Pages artifact
|
||||
env:
|
||||
BADGE_SOURCE_PATH: ${{ runner.temp }}/coverage-badge/coverage.json
|
||||
REACT_DOCTOR_BADGE_SOURCE_PATH: ${{ runner.temp }}/react-doctor-badge/react-doctor.json
|
||||
PAGES_OUTPUT_PATH: ${{ runner.temp }}/github-pages
|
||||
run: |
|
||||
mkdir -p "${PAGES_OUTPUT_PATH}/badges"
|
||||
cp "${BADGE_SOURCE_PATH}" "${PAGES_OUTPUT_PATH}/badges/coverage.json"
|
||||
cp "${REACT_DOCTOR_BADGE_SOURCE_PATH}" "${PAGES_OUTPUT_PATH}/badges/react-doctor.json"
|
||||
touch "${PAGES_OUTPUT_PATH}/.nojekyll"
|
||||
|
||||
- name: Upload Pages artifact
|
||||
|
||||
@@ -155,7 +155,7 @@ src/
|
||||
- After adding or changing tests, run `yarn test`.
|
||||
- Do not commit or force-add local rebuild output. `build/` is the main generated build output in this repo; remove or restore generated output directories after local verification before committing.
|
||||
- After React UI logic changes, run: `yarn doctor`.
|
||||
- Treat React Doctor output as actionable guidance; prioritize `error` then `warning`.
|
||||
- Treat React Doctor output as guidance for *newly introduced* issues (the CI `yarn doctor --diff` PR check flags those), not as an aggregate score to grind up: many `error`-level diagnostics flag intentional patterns or current React-Compiler limitations, not bugs. See `docs/agent-playbooks/known-surprises.md`.
|
||||
- For UI/visual changes, verify with `playwright-cli` across Chrome/Blink, Firefox/Gecko, and WebKit/Safari.
|
||||
- Cover desktop and a mobile viewport flow in each browser engine when the change affects layout, touch behavior, or responsiveness.
|
||||
- When loading, navigation, or interaction speed matters (or you cannot tell whether perf is real or just a fast dev machine), run a low-spec pass: `./scripts/pw-throttle.sh <session> mid` (or `low`) applies CPU + network throttling to a Chromium `playwright-cli` session before you measure. Throttling is Chromium-only; keep the Firefox/WebKit checks unthrottled. See `docs/agent-playbooks/low-spec-verification.md`.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[](https://github.com/bitsocialnet/5chan/actions/workflows/ci.yml)
|
||||
[](https://github.com/bitsocialnet/5chan/blob/master/scripts/write-coverage-badge.mjs)
|
||||
[](https://github.com/bitsocialnet/5chan/actions/workflows/ci.yml)
|
||||
[](https://github.com/bitsocialnet/5chan/releases/latest)
|
||||
[](https://github.com/bitsocialnet/5chan/blob/master/LICENSE)
|
||||
[](http://commitizen.github.io/cz-cli/)
|
||||
|
||||
@@ -28,6 +28,16 @@ If uncertain, ask the developer before adding an entry.
|
||||
|
||||
## Entries
|
||||
|
||||
### react-doctor score reflects React-Compiler coverage, not code health — do not chase it
|
||||
|
||||
- **Date:** 2026-06-05
|
||||
- **Observed by:** Tommaso + Claude
|
||||
- **Context:** Trying to raise the `yarn doctor` (react-doctor) score to 90 (PR #1155).
|
||||
- **What was surprising:** The score is overwhelmingly driven by React-Compiler *optimizability* diagnostics, not code quality. Most of the ~92 "errors" are the `react-hooks-js` plugin flagging valid, idiomatic code the React Compiler (v1.0) cannot optimize *yet* — `refs` (the deliberate latest-ref idiom for a stable callback) and `todo` (`try/finally` and throw-in-`try/catch` the compiler can't lower). The score also saturates on the *fraction of files with zero diagnostics*: removing 150 warnings moved it +1; suppressing all 76 compiler-bailout errors reached only 63; only suppressing essentially every rule reaches 90.
|
||||
- **Impact:** Agents/contributors can burn large effort (and risk real regressions) "fixing" the score by rewriting correct code into compiler-friendly-but-worse shapes, or by suppressing rules until the badge is meaningless. ~63 is the honest, no-regression ceiling.
|
||||
- **Mitigation:** Do NOT treat the aggregate react-doctor score as a target to grind up (the README badge was removed for this reason). Use react-doctor as a PR-diff reviewer — `yarn doctor --diff <base> --annotations`, already wired in `.github/workflows/ci.yml` — to catch *newly introduced* issues. `doctor.config.jsonc` deliberately does not enforce the `react-hooks-js` rules or `react-compiler-no-manual-memoization` (intentional patterns / current compiler limits). Only fix genuine bugs (e.g. clean `no-adjust-state-on-prop-change` cases). Full reasoning: `docs/agent-runs/react-doctor-score/`.
|
||||
- **Status:** confirmed
|
||||
|
||||
### Portless 0.11 reuses legacy proxy state unless the launcher forces HTTPS
|
||||
|
||||
- **Date:** 2026-04-28
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"task": "react-doctor-score",
|
||||
"last_updated": "2026-06-05",
|
||||
"goal": "react-doctor product-code score >= 90 with zero UI/UX regressions",
|
||||
"baseline": { "score": 55, "errors": 92, "warnings": 515, "total": 607, "files": 118, "config": "doctor.config.json excludes tests via ignore.overrides" },
|
||||
"scoring_note": "Score is density-based: removing files/issues proportionally barely moves it. Reaching 90 requires clearing most of the 607 product diagnostics. Re-measure after every phase.",
|
||||
"verification_gate": [
|
||||
"yarn type-check", "yarn lint", "yarn test", "yarn build", "yarn doctor:score",
|
||||
"playwright-cli e2e across Blink/Gecko/WebKit + mobile on touched surfaces"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "F001", "priority": 1, "status": "verified",
|
||||
"description": "Fix react-doctor config so only test files are excluded (ignore.overrides), single canonical doctor.config.json",
|
||||
"verification": ["yarn doctor:score == 55", "0 test-file diagnostics", "all product files still scored"],
|
||||
"files": ["doctor.config.json", "package.json", "react-doctor.config.json (deleted)"],
|
||||
"notes": "Committed 928784aa3. ignore.files is broken in rd 0.4.0; overrides works. Baseline 55, not 54."
|
||||
},
|
||||
{
|
||||
"id": "F002", "priority": 2, "status": "in_progress",
|
||||
"description": "Phase 1 safe bulk: delete compiler-redundant memo in non-bailout files (166) + barrel imports (39) + hoists/pure-fn/deprecated/simple-memo (~11). One agent per file.",
|
||||
"verification": ["doctor re-measure", "type-check/lint/test/build", "no new diagnostics", "playwright smoke"],
|
||||
"files": ["76 files - see /tmp/rd-phase1.json"],
|
||||
"notes": "Skip memo with side-effect factory or memo() custom comparator. Low regression risk."
|
||||
},
|
||||
{
|
||||
"id": "F003", "priority": 3, "status": "pending",
|
||||
"description": "Phase 2 compiler bailouts (errors): refs(37), set-state-in-effect(13), todo(14), preserve-manual-memoization(7), immutability(2), hooks(1), purity(1). Fix so files compile, then delete the 117 now-unlocked memo.",
|
||||
"verification": ["doctor re-measure", "full suite", "playwright e2e on touched components"],
|
||||
"files": ["29 bailout files"],
|
||||
"notes": "Behavioral risk - smaller batches, careful verification."
|
||||
},
|
||||
{
|
||||
"id": "F004", "priority": 4, "status": "pending",
|
||||
"description": "Phase 3 behavioral state bugs: no-adjust-state-on-prop-change(17), no-event-handler(30), no-pass-data-to-parent(7), prefer-useReducer(4), derived/cascading/chain state, exhaustive-deps, etc.",
|
||||
"verification": ["doctor re-measure", "full suite", "playwright e2e"],
|
||||
"files": [], "notes": "Highest UX-regression risk. Smallest batches."
|
||||
},
|
||||
{
|
||||
"id": "F005", "priority": 5, "status": "pending",
|
||||
"description": "Phase 4 structural: no-multi-comp(35), no-giant-component(12), no-many-boolean-props(12), no-render-in-render(4).",
|
||||
"verification": ["doctor re-measure", "full suite", "playwright e2e + visual"],
|
||||
"files": [], "notes": "Component splits - UI layout regression risk."
|
||||
},
|
||||
{
|
||||
"id": "F006", "priority": 6, "status": "pending",
|
||||
"description": "Phase 5 a11y + careful perf: media-has-caption(5), js-* perf refactors(~30), async-await-in-loop(13), rerender-lazy-ref-init(7), etc. Only as needed to reach 90.",
|
||||
"verification": ["doctor re-measure", "full suite", "playwright e2e"],
|
||||
"files": [], "notes": "async-* changes sequential->parallel semantics - verify carefully."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# React Doctor Score → 90 — Progress Log
|
||||
|
||||
Goal: react-doctor **product-code** score ≥ 90 with zero UI/UX regressions.
|
||||
Branch: `codex/chore/react-doctor-score`. Tracking: `feature-list.json` (this dir).
|
||||
|
||||
## Key facts
|
||||
|
||||
- React Compiler IS enabled (`babel-plugin-react-compiler`, [vite.config.ts](../../../vite.config.ts)). Deleting manual memo in files the compiler optimizes is safe — it re-memoizes automatically.
|
||||
- Score is **density-based**: excluding test files moved it only 54→55. No config shortcut to 90; must clear most of the 607 product diagnostics.
|
||||
- Baseline (after F001 config fix): **55** — 92 errors, 515 warnings, 607 total, 118 product files.
|
||||
- Verification gate per phase: `yarn type-check && yarn lint && yarn test && yarn build && yarn doctor:score`, plus playwright-cli e2e across Blink/Gecko/WebKit + mobile on touched surfaces. Never mark a phase done without re-measuring doctor AND confirming no new diagnostics + no UX regression.
|
||||
|
||||
## 2026-06-05 — F001 done
|
||||
|
||||
- Item: F001 (config fix)
|
||||
- Summary: react-doctor was scoring 82 test files because the intended ignore in `react-doctor.config.json` was shadowed by a `reactDoctor` package.json key, and `ignore.files` is broken in rd 0.4.0 (collapses scope, drops product files). Switched to a single canonical `doctor.config.json` using `ignore.overrides`, which correctly excludes only test files.
|
||||
- Files: `doctor.config.json` (new), `package.json` (removed reactDoctor key), `react-doctor.config.json` (deleted).
|
||||
- Verification: `yarn doctor:score` == 55; 0 test-file diagnostics; all 118 product files still scored; `yarn lint` clean. Committed `928784aa3`.
|
||||
- Next: F002 Phase 1 safe bulk (memo deletions in non-bailout files + barrel imports + small mechanical) via workflow, one agent per file.
|
||||
|
||||
## 2026-06-05 — F002 Phase 1 pilot (9 files) + scaled run launched
|
||||
|
||||
- Item: F002. Piloted 9 representative files via a fix+verify workflow, then launched the scaled run (47 files).
|
||||
- **Calibration (critical):** ~12–13 warning fixes moved the score only 55→56. Score is steeply density-based and integer-rounded. Reaching 90 means clearing the large majority of the 607 product issues; errors (92) likely weigh more than warnings.
|
||||
- **Two hazard classes the pilot caught (rules now baked into the workflow):**
|
||||
1. `react-compiler-no-manual-memoization` is NOT always safe. A `useMemo` whose factory reads EXTERNAL MUTABLE STATE (DOM/getComputedStyle/theme/refs) with deps NOT referenced in the factory body is load-bearing — deleting it makes the value stale (no recompute on theme/resize). Signature: deps array lists vars unused in the body; the `void dep;` pattern marks deliberate retention. Reference: `catalog.tsx` catalogMetrics. The pilot's `use-reply-height-estimates.ts` metrics memo was wrongly deleted and restored.
|
||||
2. `no-barrel-import` rewrites BREAK `vi.mock('<barrel>')` test mocks (board-buttons→catalog-filters, board-header→tooltip). **Decision: barrel fixes are DROPPED from Phase 1** (low value for a tree-shaken Vite bundle, high test-breakage risk). Revisit later only if score-points require it, with matching mock updates.
|
||||
- Verification: pilot type-check/lint clean; 3 tests failed (barrel-mock breakage) → reverted 2 barrel changes → 18 tests pass. Score 56. Net kept ~12 safe fixes (memo deletions, pure-fn hoists, regex/cache).
|
||||
- Files committed/changed so far in working tree (pilot, not yet committed): board-blotter, board-buttons, code-block, crypto-address-setting, reply-quote-utils, use-popular-threads-options-store, use-reply-height-estimates.
|
||||
- Next: process scaled-run results (fix flagged regressions), run full gate (type-check/lint/test/build/doctor) + browser e2e, commit Phase 1, re-measure, then STRATEGIC CHECKPOINT with user on cost/risk of pushing to 90.
|
||||
|
||||
## 2026-06-05 — PIVOTAL FINDING: score is error-dominated; warnings are inert
|
||||
|
||||
- The scaled 45-file memo-deletion run **removed 150 warnings and fully cleaned 30 files but moved the score only 55→56 (+1)**. Definitive proof: react-doctor's score is overwhelmingly weighted on ERRORS, not warnings. The 515 warnings (memo/barrel/structural) are nearly irrelevant to the number.
|
||||
- **The 92 errors are essentially the entire gap from 90.** Breakdown (30 files): refs 37, no-adjust-state-on-prop-change 17, todo(compiler-syntax) 14, set-state-in-effect 13, preserve-manual-memoization 7, immutability 2, hooks 1, purity 1. ALL are behavioral / compiler-structural — the high-regression-risk tier.
|
||||
- Consequence: there is **no low-risk path to 90**. Memo/warning cleanup (Phase 1) cannot move the score. Reaching 90 requires carefully fixing ~all 92 behavioral error-sites — real bug fixes, but each carries execution/regression risk. This directly tensions the "no UI/UX regressions" constraint.
|
||||
- The scaled run was REVERTED: it added risk (external-number-quote-link useCallback deletion → useEffect loop → test worker timeout) and a workflow reliability problem (45/47 fix agents edited files but failed to return StructuredOutput, so the verify stage never ran) — all for ~zero score gain.
|
||||
- Workflow lesson: fix agents doing heavy multi-edit work + a complex output schema frequently skip the final StructuredOutput call. Future error-fixing batches must be SMALL (few files), simpler schema, and verify must run.
|
||||
- State after revert: working tree = validated pilot only (7 files), 1051 tests pass, type-check/lint clean, score 56.
|
||||
- DECISION REQUIRED from user before proceeding: pursue 90 via the 92 risky error fixes (high cost+risk), fix safe-only errors and report the ceiling, or reassess the target.
|
||||
|
||||
## 2026-06-05 — RESOLUTION: 90 is unreachable honestly; landed at 63
|
||||
|
||||
- Calibration proved the score is NOT count-based — it saturates on **fraction of files with zero diagnostics**. Suppressing all 76 compiler-bailout errors only reached 63; suppressing the 76 errors + 289 biggest warnings still only 63 (85 files affected); suppressing ALL rules = 100. So 90 requires ~all files diagnostic-free, i.e. effectively disabling react-doctor — a meaningless badge.
|
||||
- Most of the 92 "errors" flag legitimate, correct patterns the React Compiler can't optimize: `refs` (37) = the deliberate latest-ref idiom; `todo` (14) = try/finally & throw-in-try/catch the compiler can't lower; rewriting them degrades working code.
|
||||
- The only genuine bugs are `no-adjust-state-on-prop-change` (17), but ~15 of them are entangled with legitimate side effects (navigate(), ref.cancel(), async fetch orchestration) that belong in effects — not cleanly movable to render without restructuring critical/media flows.
|
||||
- User chose (after full disclosure): **honest high-60s, no regressions.** Final approach:
|
||||
1. `doctor.config.jsonc` (replaces .json): documented policy to NOT enforce the React-Compiler lint rules (`react-hooks-js/*` + `react-compiler-no-manual-memoization`) — they flag intentional patterns/compiler limits, not bugs. Keeps all real code-quality/a11y/perf rules enforced.
|
||||
2. Fixed the one cleanly-safe state-sync bug (`use-now-seconds`, render-time refresh).
|
||||
3. Left the other 15 no-adjust bugs documented as real-but-entangled (follow-up), not force-fixed (would risk regressions in publish/media flows).
|
||||
- Result: score **63** (from broken-config 54 / real-baseline 55). type-check 0, lint 0/0, 1051 tests pass. Committed: config-fix 928784aa3, memo-cleanup d72190164; pending: config policy + use-now-seconds.
|
||||
- NOTE: /goal was set to >=90 which is not honestly achievable — user to `/goal clear`.
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
// react-doctor configuration.
|
||||
"diff": false,
|
||||
"ignore": {
|
||||
// Exclude test files from scoring. NOTE: ignore.files is broken in react-doctor 0.4.0
|
||||
// (any non-empty value collapses the scan scope and drops product files); ignore.overrides
|
||||
// with no `rules` correctly suppresses every rule for the matched (test) files.
|
||||
"overrides": [{ "files": ["**/__tests__/**", "**/*.test.*"] }],
|
||||
|
||||
// Policy: we use the React Compiler (babel-plugin-react-compiler, see vite.config.ts) but do
|
||||
// NOT lint-enforce the React-Compiler optimizability rules. The `react-hooks-js` plugin rules
|
||||
// flag intentional, correct patterns (e.g. the latest-ref idiom for a stable callback) and
|
||||
// current compiler limitations (e.g. try/finally and throw-in-try/catch the compiler cannot
|
||||
// lower yet) rather than real bugs; rewriting working code to satisfy them would degrade it.
|
||||
// `react-compiler-no-manual-memoization` is likewise advisory given the compiler.
|
||||
"rules": [
|
||||
"react-hooks-js/refs",
|
||||
"react-hooks-js/todo",
|
||||
"react-hooks-js/set-state-in-effect",
|
||||
"react-hooks-js/preserve-manual-memoization",
|
||||
"react-hooks-js/immutability",
|
||||
"react-hooks-js/hooks",
|
||||
"react-hooks-js/purity",
|
||||
"react-doctor/react-compiler-no-manual-memoization"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,6 @@
|
||||
"doctor": "react-doctor . -y",
|
||||
"doctor:score": "react-doctor . --score -y",
|
||||
"doctor:verbose": "react-doctor . --verbose -y",
|
||||
"doctor:badge": "node scripts/write-react-doctor-badge.mjs",
|
||||
"contract:imgur": "cd android && ./gradlew :app:connectedDebugAndroidTest -Pandroid.experimental.androidTest.useUnifiedTestPlatform=false -Pandroid.testInstrumentationRunnerArguments.class=fivechan.android.MediaUploadAutomationRunnerTest",
|
||||
"smoke:upload-selectors": "node scripts/smoke-upload-selectors.js",
|
||||
"test:coverage": "vitest run --fileParallelism=false --coverage.enabled --coverage.provider=istanbul --coverage.reporter=text --coverage.reporter=json --coverage.reporter=json-summary --coverage.reportsDirectory=./coverage",
|
||||
@@ -259,8 +258,5 @@
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
},
|
||||
"reactDoctor": {
|
||||
"diff": false
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -31,7 +31,6 @@ Source: https://github.com/bitsocialnet/5chan/blob/master/README.md
|
||||
```markdown
|
||||
[](https://github.com/bitsocialnet/5chan/actions/workflows/ci.yml)
|
||||
[](https://github.com/bitsocialnet/5chan/blob/master/scripts/write-coverage-badge.mjs)
|
||||
[](https://github.com/bitsocialnet/5chan/actions/workflows/ci.yml)
|
||||
[](https://github.com/bitsocialnet/5chan/releases/latest)
|
||||
[](https://github.com/bitsocialnet/5chan/blob/master/LICENSE)
|
||||
[](http://commitizen.github.io/cz-cli/)
|
||||
@@ -326,7 +325,7 @@ src/
|
||||
- After adding or changing tests, run `yarn test`.
|
||||
- Do not commit or force-add local rebuild output. `build/` is the main generated build output in this repo; remove or restore generated output directories after local verification before committing.
|
||||
- After React UI logic changes, run: `yarn doctor`.
|
||||
- Treat React Doctor output as actionable guidance; prioritize `error` then `warning`.
|
||||
- Treat React Doctor output as guidance for *newly introduced* issues (the CI `yarn doctor --diff` PR check flags those), not as an aggregate score to grind up: many `error`-level diagnostics flag intentional patterns or current React-Compiler limitations, not bugs. See `docs/agent-playbooks/known-surprises.md`.
|
||||
- For UI/visual changes, verify with `playwright-cli` across Chrome/Blink, Firefox/Gecko, and WebKit/Safari.
|
||||
- Cover desktop and a mobile viewport flow in each browser engine when the change affects layout, touch behavior, or responsiveness.
|
||||
- When loading, navigation, or interaction speed matters (or you cannot tell whether perf is real or just a fast dev machine), run a low-spec pass: `./scripts/pw-throttle.sh <session> mid` (or `low`) applies CPU + network throttling to a Chromium `playwright-cli` session before you measure. Throttling is Chromium-only; keep the Firefox/WebKit checks unthrottled. See `docs/agent-playbooks/low-spec-verification.md`.
|
||||
@@ -797,6 +796,16 @@ If uncertain, ask the developer before adding an entry.
|
||||
|
||||
## Entries
|
||||
|
||||
### react-doctor score reflects React-Compiler coverage, not code health — do not chase it
|
||||
|
||||
- **Date:** 2026-06-05
|
||||
- **Observed by:** Tommaso + Claude
|
||||
- **Context:** Trying to raise the `yarn doctor` (react-doctor) score to 90 (PR #1155).
|
||||
- **What was surprising:** The score is overwhelmingly driven by React-Compiler *optimizability* diagnostics, not code quality. Most of the ~92 "errors" are the `react-hooks-js` plugin flagging valid, idiomatic code the React Compiler (v1.0) cannot optimize *yet* — `refs` (the deliberate latest-ref idiom for a stable callback) and `todo` (`try/finally` and throw-in-`try/catch` the compiler can't lower). The score also saturates on the *fraction of files with zero diagnostics*: removing 150 warnings moved it +1; suppressing all 76 compiler-bailout errors reached only 63; only suppressing essentially every rule reaches 90.
|
||||
- **Impact:** Agents/contributors can burn large effort (and risk real regressions) "fixing" the score by rewriting correct code into compiler-friendly-but-worse shapes, or by suppressing rules until the badge is meaningless. ~63 is the honest, no-regression ceiling.
|
||||
- **Mitigation:** Do NOT treat the aggregate react-doctor score as a target to grind up (the README badge was removed for this reason). Use react-doctor as a PR-diff reviewer — `yarn doctor --diff <base> --annotations`, already wired in `.github/workflows/ci.yml` — to catch *newly introduced* issues. `doctor.config.jsonc` deliberately does not enforce the `react-hooks-js` rules or `react-compiler-no-manual-memoization` (intentional patterns / current compiler limits). Only fix genuine bugs (e.g. clean `no-adjust-state-on-prop-change` cases). Full reasoning: `docs/agent-runs/react-doctor-score/`.
|
||||
- **Status:** confirmed
|
||||
|
||||
### Portless 0.11 reuses legacy proxy state unless the launcher forces HTTPS
|
||||
|
||||
- **Date:** 2026-04-28
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"diff": false,
|
||||
"ignore": {
|
||||
"files": ["**/__tests__/**", "**/*.test.*"]
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const CWD = process.cwd();
|
||||
const BADGE_OUTPUT_PATH = path.join(CWD, "badges", "react-doctor.json");
|
||||
const reactDoctorArgs = ["react-doctor", ".", "--json", "--json-compact", "--yes", "--fail-on", "none"];
|
||||
|
||||
console.log(`[react-doctor-badge] Running "yarn ${reactDoctorArgs.join(" ")}" in "${CWD}".`);
|
||||
|
||||
const reportText = execFileSync("yarn", reactDoctorArgs, {
|
||||
cwd: CWD,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "inherit"],
|
||||
});
|
||||
|
||||
let report;
|
||||
try {
|
||||
report = JSON.parse(reportText);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[react-doctor-badge] Failed to parse React Doctor JSON report: ${message}`);
|
||||
console.error(`[react-doctor-badge] Output preview: ${reportText.slice(0, 500)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const score = report?.summary?.score;
|
||||
|
||||
if (typeof score !== "number") {
|
||||
console.error("[react-doctor-badge] Missing summary.score in React Doctor JSON report.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const color = score >= 90 ? "brightgreen" : score >= 75 ? "green" : score >= 50 ? "yellow" : "red";
|
||||
const badge = {
|
||||
schemaVersion: 1,
|
||||
label: "react doctor",
|
||||
message: `${score}/100`,
|
||||
color,
|
||||
};
|
||||
|
||||
fs.mkdirSync(path.dirname(BADGE_OUTPUT_PATH), { recursive: true });
|
||||
fs.writeFileSync(BADGE_OUTPUT_PATH, `${JSON.stringify(badge, null, 2)}\n`);
|
||||
|
||||
console.log(`[react-doctor-badge] Wrote "${BADGE_OUTPUT_PATH}" with score ${score}/100.`);
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import blotterData from '../../data/5chan-blotter.json';
|
||||
import BlotterMessage from '../blotter-message';
|
||||
import BlotterMessage from '../blotter-message/blotter-message';
|
||||
import { formatBlotterDate, getBlotterPreview, isBlotterEntry, sortBlotterEntries } from '../../lib/utils/blotter-utils';
|
||||
import useBlotterVisibilityStore from '../../stores/use-blotter-visibility-store';
|
||||
import styles from './board-blotter.module.css';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useAccount, useComment, useSubscribe } from '@bitsocial/bitsocial-react-hooks';
|
||||
@@ -225,19 +224,16 @@ const HiddenCatalogThreadsToggle = ({
|
||||
const filteredDirectoryAddresses = useFilteredDirectoryAddresses();
|
||||
const sortType = useSortingStore((state) => state.sortType);
|
||||
const toggleShownScopeKey = useHiddenCatalogThreadsStore((state) => state.toggleShownScopeKey);
|
||||
const communityAddresses = useMemo(() => {
|
||||
if (isInAllView) {
|
||||
return filteredDirectoryAddresses;
|
||||
}
|
||||
if (isInSubscriptionsView) {
|
||||
return account?.subscriptions?.filter(Boolean) || EMPTY_COMMUNITY_ADDRESSES;
|
||||
}
|
||||
if (isInModView) {
|
||||
return accountCommunityAddresses;
|
||||
}
|
||||
|
||||
return address ? [address] : EMPTY_COMMUNITY_ADDRESSES;
|
||||
}, [account?.subscriptions, accountCommunityAddresses, address, filteredDirectoryAddresses, isInAllView, isInModView, isInSubscriptionsView]);
|
||||
let communityAddresses: string[];
|
||||
if (isInAllView) {
|
||||
communityAddresses = filteredDirectoryAddresses;
|
||||
} else if (isInSubscriptionsView) {
|
||||
communityAddresses = account?.subscriptions?.filter(Boolean) || EMPTY_COMMUNITY_ADDRESSES;
|
||||
} else if (isInModView) {
|
||||
communityAddresses = accountCommunityAddresses;
|
||||
} else {
|
||||
communityAddresses = address ? [address] : EMPTY_COMMUNITY_ADDRESSES;
|
||||
}
|
||||
const { hiddenCatalogThreads, isLoadingHiddenCatalogThreads, scopeKey } = useHiddenCatalogThreads({
|
||||
communityAddresses,
|
||||
sortType: sortType === 'new' ? 'new' : 'active',
|
||||
@@ -295,11 +291,12 @@ export const AutoButton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' });
|
||||
};
|
||||
|
||||
export const BottomButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const scrollToBottom = () => {
|
||||
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' });
|
||||
};
|
||||
return (
|
||||
<button type='button' className='button' onClick={scrollToBottom}>
|
||||
{t('bottom')}
|
||||
@@ -307,11 +304,12 @@ export const BottomButton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
|
||||
};
|
||||
|
||||
export const TopButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
|
||||
};
|
||||
return (
|
||||
<button type='button' className='button' onClick={scrollToTop}>
|
||||
{t('top')}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useMemo } from 'react';
|
||||
import { highlightCode } from '../../lib/utils/code-highlight';
|
||||
import styles from './code-block.module.css';
|
||||
|
||||
@@ -9,7 +8,7 @@ import styles from './code-block.module.css';
|
||||
* inline markdown <span>.
|
||||
*/
|
||||
const CodeBlock = ({ source }: { source: string }) => {
|
||||
const tokens = useMemo(() => highlightCode(source), [source]);
|
||||
const tokens = highlightCode(source);
|
||||
|
||||
return (
|
||||
<code className={styles.code}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAccount, setAccount, useResolvedAuthorAddress } from '@bitsocial/bitsocial-react-hooks';
|
||||
import styles from './crypto-address-setting.module.css';
|
||||
@@ -244,4 +244,4 @@ const CryptoAddressSetting = () => {
|
||||
return <CryptoAddressSettingContent key={accountResetKey} account={account} />;
|
||||
};
|
||||
|
||||
export default memo(CryptoAddressSetting);
|
||||
export default CryptoAddressSetting;
|
||||
|
||||
@@ -6,13 +6,21 @@ const getNowSeconds = () => Date.now() / 1000;
|
||||
|
||||
export const useNowSeconds = (enabled = true) => {
|
||||
const [nowSeconds, setNowSeconds] = useState(getNowSeconds);
|
||||
const [prevEnabled, setPrevEnabled] = useState(enabled);
|
||||
|
||||
// Refresh during render when (re-)enabled, instead of via an effect, to avoid
|
||||
// an extra render showing a stale value. The interval keeps it fresh after.
|
||||
if (enabled !== prevEnabled) {
|
||||
setPrevEnabled(enabled);
|
||||
if (enabled) {
|
||||
setNowSeconds(getNowSeconds());
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
const updateNow = () => setNowSeconds(getNowSeconds());
|
||||
updateNow();
|
||||
const interval = window.setInterval(updateNow, STATUS_REFRESH_INTERVAL_MS);
|
||||
const interval = window.setInterval(() => setNowSeconds(getNowSeconds()), STATUS_REFRESH_INTERVAL_MS);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [enabled]);
|
||||
|
||||
|
||||
@@ -26,30 +26,26 @@ const useReplyHeightEstimates = ({ directRepliesByParentCid, enabled = true, isM
|
||||
const location = useLocation();
|
||||
const windowWidth = useWindowWidth();
|
||||
const themeKey = typeof document !== 'undefined' ? document.body.className : '';
|
||||
const effectiveMode = useMemo(() => mode ?? resolveReplyVirtualizationMode(location.search), [location.search, mode]);
|
||||
const effectiveMode = mode ?? resolveReplyVirtualizationMode(location.search);
|
||||
|
||||
const metrics = useMemo(() => readReplyTypographyMetrics(), [themeKey, windowWidth]);
|
||||
|
||||
const rawHeightEstimates = useMemo(
|
||||
() =>
|
||||
!enabled
|
||||
? []
|
||||
: getReplyHeightEstimates({
|
||||
context: 'thread',
|
||||
directRepliesByParentCid,
|
||||
isMobile,
|
||||
maxContentChars,
|
||||
metrics,
|
||||
quotedByMap,
|
||||
replies,
|
||||
windowWidth,
|
||||
}),
|
||||
[directRepliesByParentCid, enabled, isMobile, maxContentChars, metrics, quotedByMap, replies, windowWidth],
|
||||
);
|
||||
const rawHeightEstimates = !enabled
|
||||
? []
|
||||
: getReplyHeightEstimates({
|
||||
context: 'thread',
|
||||
directRepliesByParentCid,
|
||||
isMobile,
|
||||
maxContentChars,
|
||||
metrics,
|
||||
quotedByMap,
|
||||
replies,
|
||||
windowWidth,
|
||||
});
|
||||
|
||||
const heightEstimates = effectiveMode === 'off' ? undefined : rawHeightEstimates;
|
||||
const defaultItemHeight = useMemo(() => getTypicalReplyHeight(rawHeightEstimates, isMobile), [rawHeightEstimates, isMobile]);
|
||||
const itemSize = useMemo<SizeFunction | undefined>(() => (effectiveMode === 'item-size' ? getReplyItemSizeFromElement : undefined), [effectiveMode]);
|
||||
const defaultItemHeight = getTypicalReplyHeight(rawHeightEstimates, isMobile);
|
||||
const itemSize: SizeFunction | undefined = effectiveMode === 'item-size' ? getReplyItemSizeFromElement : undefined;
|
||||
|
||||
return { defaultItemHeight, heightEstimates, itemSize, metrics, mode: effectiveMode, windowWidth };
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { QUOTE_NUMBER_REGEX } from './url-utils';
|
||||
|
||||
const QUOTE_NUMBER_REGEX_GLOBAL = new RegExp(QUOTE_NUMBER_REGEX.source, 'g');
|
||||
|
||||
export const getQuotedCidsFromContent = (content: string | undefined, numberToCid: Record<number, string> | undefined) => {
|
||||
if (!content || !numberToCid) return undefined;
|
||||
const cids = new Set<string>();
|
||||
for (const match of content.matchAll(new RegExp(QUOTE_NUMBER_REGEX.source, 'g'))) {
|
||||
for (const match of content.matchAll(QUOTE_NUMBER_REGEX_GLOBAL)) {
|
||||
const num = parseInt(match[1], 10);
|
||||
const cid = numberToCid[num];
|
||||
if (cid) cids.add(cid);
|
||||
|
||||
@@ -7,8 +7,10 @@ interface PopularThreadsOptionsStore {
|
||||
setShowNsfwContentOnly: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const storedShowWorksafeContentOnly = localStorage.getItem('showWorksafeContentOnly');
|
||||
|
||||
const usePopularThreadsOptionsStore = create<PopularThreadsOptionsStore>((set) => ({
|
||||
showWorksafeContentOnly: localStorage.getItem('showWorksafeContentOnly') === 'true' || localStorage.getItem('showWorksafeContentOnly') === null ? true : false,
|
||||
showWorksafeContentOnly: storedShowWorksafeContentOnly === 'true' || storedShowWorksafeContentOnly === null ? true : false,
|
||||
setShowWorksafeContentOnly: (value: boolean) => {
|
||||
set({ showWorksafeContentOnly: value });
|
||||
localStorage.setItem('showWorksafeContentOnly', value.toString());
|
||||
|
||||
Reference in New Issue
Block a user