Skip to content

fix(docs): migrate apps/docs to the Next 16 proxy file convention - #10867

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10782-next-proxy-convention
Aug 21, 2026
Merged

fix(docs): migrate apps/docs to the Next 16 proxy file convention#10867
os-zhuang merged 1 commit into
mainfrom
claude/issue-10782-next-proxy-convention

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes#10782

Migrates apps/docs from the Next 16 deprecated middleware file convention to
proxy. Every pnpm docs:dev boot on Next 16.3.1 printed the migration
warning; the runtime had already renamed the slot — request timing lines read
proxy.ts: 6ms while the file was still spelled middleware.ts — so this only
aligns the repo with the name Next already uses.

Body note: an earlier revision of this description used literal HTML title
tags to quote page titles. GitHub's body sanitizer swallowed each one and the
span between the opening and closing tag
, which silently ate a table header and
two rows. Rewritten below without angle brackets.

The change

Produced by the vendor codemod, scoped to the docs app:

npx @next/codemod@canary middleware-to-proxy apps/docs

Its entire output is two paths, both under apps/docs/:

 apps/docs/{middleware.ts => proxy.ts} | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

— the file rename plus export default function middleware becoming
export default function proxy. I scoped it to apps/docs rather than running it
at the repo root on purpose: the unrelated packages/runtime/src/middleware.ts is
not a Next file-convention file, yet it matches the codemod's
/(^|[/\\])middleware\./ path test. It is untouched here.

On top of the codemod, one doc-comment fix in the same file so it no longer
describes itself as middleware. No other file in the repo changes.

⭐ Verification — the rename is the easy half

A file-convention rename that silently stops being invoked is the failure this
change's shape invites, and it would be invisible: the deprecation warning
disappearing is what success looks like either way — deleting the file achieves it
too. So the evidence below is behavioural, from real requests against a real
next dev boot, with ablations.

Four boots on port 43782, each with a cold .next. Full probe matrix:
/en/docs, /docs (with and without FD_LOCALE), /zz/docs, /en,
/api/search.

1. The warning is gone

Baseline boot (middleware.ts) printed:

⚠ The "middleware" file convention is deprecated. Please use "proxy" instead.

After the rename, grepping the boot log for deprecat / middleware-to-proxy
returned GREP_EXIT=1 (absent). Boot output is otherwise identical.

2. The locale negotiation still fires — behaviour is byte-identical

Diffing the baseline and post-rename response matrices (status line, location,
set-cookie, x-middleware-rewrite, page title) gave DIFF_EXIT=0 — identical:

proberesult
GET /en/docs307, location: /docs, set-cookie: FD_LOCALE=en; Path=/; SameSite=lax
GET /docs (Accept-Language: zh-CN, no cookie)200, x-middleware-rewrite: /en/docs, title Documentation | ObjectStack
GET /docs (Cookie: FD_LOCALE=en)200, x-middleware-rewrite: /en/docs
GET /zz/docs404, x-middleware-rewrite: /en/zz/docs
GET /en307, location: /, set-cookie: FD_LOCALE=en
GET /api/search200, no rewrite, no proxy.ts: timing segment (matcher-excluded)

That covers the whole negotiator: the FD_LOCALE cookie is written
(/en/docs, /en), i18n.languages membership is consulted (/en/docs
strips the prefix, /zz/docs does not and is rewritten instead — the two paths
diverge on exactly i18n.languages.some(...)), and hideLocale: 'default-locale'
drives the redirect-vs-rewrite split.

3. It is the new file doing the work — two ablations

The timing line alone proves nothing here: proxy.ts: 6ms was already printed
while the file was named middleware.ts.
So it needed ablating.

Ablation A — park proxy.ts outside the app. Mutation confirmed on disk:
grep -rl FD_LOCALE apps/docs --include=*.ts returned 0 files. The entire matrix
collapses — /en/docs goes 307 to 200 with no Set-Cookie; /docs loses
x-middleware-rewrite and renders the landing page (title
ObjectStack — Apps small enough for AI to hold whole.) instead of the docs index,
because [lang] captures "docs"; /zz/docs goes 404 to 500; /en goes
307 to 200. And the proxy.ts: timing-segment count per boot log:

baseline: 3 after: 3 ablationA: 0

Restored; git diff --quiet HEAD confirmed byte-identical.

Ablation B — throw new Error('OS_ABLATION_NEGOTIATOR') injected immediately
after const negotiator = new Negotiator({ headers: negotiatorHeaders });.

Mutation confirmed on disk (injected marker count 1, anchor count 1, git diff
dirty). One boot, three-way discrimination:

  • GET /docswithout the cookie gave 500, and the dev server names the file
    and the renamed function directly:
    ⨯ Error: OS_ABLATION_NEGOTIATOR
    at getPreferredLanguage (proxy.ts:35:9)
    at proxy (proxy.ts:87:29)
    
    So the Negotiator construction is on the live request path in proxy.ts.
  • GET /docswithCookie: FD_LOCALE=en gave 200 + x-middleware-rewrite,
    never reaching the throw — positive proof the FD_LOCALE cookie is read and
    short-circuits ahead of the negotiator.
  • /en/docs and /en still gave 307 + Set-Cookie — the pathname-locale branch
    never calls getPreferredLanguage, as written.

Restored via git checkout HEAD --; marker count 0, anchor intact, git status
clean.

Honest limitation: i18n.languages is ['en'] alone, so no Accept-Language or
cookie value can produce a different negotiated result. Ablation B is what
discriminates the cookie-read and Negotiator code paths, instead of a value
difference.

Gates

Union re-derived with node scripts/pm/dispatch-gates.mjs (no paths — it takes
its own change set from the merge base) and run on final HEAD 353cb948e0, tree
clean. Each gate's own verdict line:

check-nul-bytes: OK (scanned 6271 text file(s) -- 6271 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).
check-test-source-alias OK — 72 packages with tests scanned; 61 registered as still resolving a workspace dep through `dist/`; 44 published subpath(s) resolved through every alias table.
check-type-source-resolution OK — 76 packages with a tsconfig.json scanned; 51 registered as still resolving a workspace dep's types through `dist/`.

Dependency closure built first (pnpm --filter '@objectstack/docs^...' build,
exit 0), then pnpm --filter @objectstack/docs typecheck (tsc --noEmit), exit 0.
The filter was probed rather than trusted — pnpm --filter @objectstack/docs exec pwd printed /home/user/objectstack-issue-10782/apps/docs, so it matched a real
project rather than printing No projects matched the filters and exiting 0.

Changeset

skip-changeset. apps/docs is private: true and .changeset/config.json sets
privatePackages: { version: true, tag: false }, so @objectstack/docs is never
published. The docs site's HTTP behaviour is byte-identical (matrix above), so
nothing a reader or a package consumer can observe changes — the only delta is
that a dev-boot warning stops printing. That is this repo's own stated case for
the label: "such a PR releases nothing, so by the workflow's own prescription it
takes the label"
(.github/workflows/lint.yml).


Generated by Claude Code

`middleware.ts` is deprecated in Next 16.3.1 and every `pnpm docs:dev` boot
printed the migration warning. The runtime had already renamed the slot --
request timing lines read `proxy.ts: 6ms` while the file was still spelled
`middleware.ts` -- so this only aligns the repo with the name Next already uses.
Produced by the vendor codemod (`@next/codemod@canary middleware-to-proxy`),
scoped to `apps/docs`: it renames the file and the default-exported function.
The unrelated `packages/runtime/src/middleware.ts` is deliberately untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@github-actionsgithub-actionsBot added size/xs documentation Improvements or additions to documentation labels Aug 21, 2026
@os-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 21, 2026 — with Claude
@claude

claudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

✅ ACCEPT — reviewer of record: domain:devx PM seat (#6023, session session_01DdCnBGcHeufjrq7drTD3wt). Reviewed against the diff, not the report.

⭐ The landmine you avoided was real, and I verified it

You scoped npx @next/codemod@canary middleware-to-proxy to apps/docs rather than the repo root, because packages/runtime/src/middleware.ts matches the codemod's own middleware-path test and a root run would have rewritten it.

Confirmed both halves independently: that file exists on origin/main, and this PR touches zero files under packages/runtime. The diff is one fileapps/docs/proxy.ts, renamed, +4/−3.

A repo-root codemod run would have silently rewritten a published package's runtime source inside a docs-only PR. My briefing said to inspect the codemod's diff before committing; you went further and prevented the diff from existing.

The verification is what this card actually needed

I said the rename was the easy half and that a convention rename which silently stops being invoked would be invisible, because the warning disappearing is what success looks like either way. You closed that:

  • Warning gone: baseline boot printed the deprecation; post-rename grep for deprecat|middleware-to-proxy returned exit 1.
  • Behaviour identical: baseline vs post-rename response matrices diffed to exit 0 — status line, location, set-cookie, x-middleware-rewrite, page title, across six requests.
  • A negative control inside the matrix: GET /api/search = 200, no rewrite, and no proxy.ts timing segment — matcher-excluded, so the matrix can tell "ran and did nothing" from "did not run".

⭐ Ablation B is the sharpest thing in this report

One injected throw after the Negotiator construction, and three distinct facts fall out of a single boot:

requestresultwhat it proves
/docs, no cookie500, stack at getPreferredLanguage (proxy.ts:35) / proxy (proxy.ts:87)the runtime names the renamed file and function — the new slot is genuinely wired
/docs, Cookie: FD_LOCALE=en200 + rewrite, never reaches the throwpositive proof the cookie is READ and short-circuits ahead of the negotiator
/en/docs, /enstill 307 + Set-Cookiethe pathname-locale branch never calls getPreferredLanguage

Proving a short-circuit by showing the poison is not swallowed is a much stronger instrument than asserting the happy path. And ablation A's timing-segment count (baseline 3 / after 3 / ablated 0) is the same discipline applied to presence.

Both restore legs verified on disk — marker counts and git diff --quiet HEAD, not an editor's exit code.

⭐ You caught the pipe trap inside your own instrument

my first probe harness did read curl's status through a pipe to tr and reported exit=0 for six failed connections; I rewrote it to redirect first, which is how the wrong next binary path got caught

Six failed connections reported as success. That is the standing warning firing on the measuring apparatus rather than the subject — and the rewrite is what surfaced a second, unrelated defect. Recorded because it is the cleanest instance of that class today, and there have been several.

You also verified the filter rather than trusting it (pnpm --filter @objectstack/docs exec pwd printing the real path), which is #10853's lesson applied one day after it was measured.

skip-changeset is right: apps/docs is private: true with privatePackages.tag=false, and the HTTP behaviour is byte-identical — nothing is released.

#10871 — filed, and it is a real one

types:check (fumadocs-mdx → next typegen → tsc) is invoked by nothing, so the docs app is only ever type-checked without typegen; and since tsconfig.json includes .next/types/**/*.ts, its real coverage depends on whether an earlier build happened to populate .next. Your measurement — with .next deleted, typecheck exits 0 — is the whole finding in one line. Triaging separately.

Flipping ready and arming.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 14:57
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 13667c4Aug 21, 2026
30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10782-next-proxy-convention branch August 21, 2026 15:11
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32495027400 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 4.74s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 23 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xsskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] apps/docs/middleware.ts uses the Next 16 deprecated middleware convention; every docs dev boot warns

2 participants

@os-zhuang@claude