Skip to content

CI type-check can fail with TS2306 ".source/server.ts is not a module": next typegen exits while fumadocs-mdx's un-awaited regeneration is still writing .source #311

Description

@objectstack-fleet

Found by the repo:objectos seat on 2026-10-06, when the first main run after PR #310 went red. Priority: P1. A red build on main skips the deploy, so a landed change is not published.

What happened

  • The failure: CI run 37478051451 (push of 5d2f837, PR docs: contrast, phone table affordance, FAQ headings, title-weighted per-locale search, consistency pass (#301) #310's squash), attempt 1. Its build job failed at pnpm turbo run type-check:
    [MDX] generated files in 63.02571499999999ms
    Generating route types...
    ✓ Types generated successfully
    lib/source.ts(1,22): error TS2306: File '/home/runner/work/objectos/objectos/apps/docs/.source/server.ts' is not a module.
    
  • The same tree passed: 5d2f837 and the PR head 911a174 have the same tree, 263e370. The PR's run 37476364739 passed this step 14 minutes earlier.
  • The re-run passed: attempt 2 of the same run passed build.
  • The record: this is the first such failure in the last 40 CI runs.

Root cause (read from the installed source)

apps/docs runs "type-check": "fumadocs-mdx && next typegen && tsc --noEmit".

  1. fumadocs-mdx (the CLI) writes .source/* and awaits the writes.
  2. next typegen loads next.config.mjs, which calls createMDX(). In fumadocs-mdx 15.0.7, dist/next/index.js:14-20, createMDX() calls init(isDev, core) without awaiting it. init recompiles source.config.ts and calls core.emit({ write: true }), which rewrites every .source/* file with fs.writeFile (dist/core-DlDe_Eze.js:232-236). writeFile truncates the file first.
  3. If next typegen finishes before that floating promise has written the file, the process exits with .source/server.ts truncated. tsc then reads an empty file, which is "not a module".

The two logs show the difference.

  • The passing PR run prints [MDX] generated files twice before ✓ Types generated successfully: once from the CLI, once from createMDX's write inside typegen.
  • The failing run prints it only once: typegen exited before its own regeneration finished.

Why it showed now: PR #310 made source.config.ts import rehypeCodeDefaultOptions from fumadocs-core/mdx-plugins, for the Shiki colorReplacements. That makes the config that init recompiles heavier to load, which plausibly widens the window. Before #310 the file imported only fumadocs-mdx/config, fumadocs-core/source/schema and node:path.

Done looks like

  • The step cannot end with a truncated .source. For example, reorder it to next typegen && fumadocs-mdx && tsc --noEmit, so the awaited CLI write is the last writer. Or use another fix that removes the race rather than shrinking it. State the choice, and check that next build, dev and postinstall are not exposed in the same way.
  • The race is reproduced red before the fix, for example by slowing init's write or by looping the step, and shown unable to occur after.
  • Repo gates are green, with the CI-shaped type-check and build.
  • No fumadocs-mdx patch, unless it is the only fix. An upstream report is welcome, from public facts only.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop, repo:objectos seat
    Session: session_01FeA1nwBz1ohH65dvffUGKr
    Account: hotlong (the seat's linked user, as GET /user answers it; always the card's assignee)
    Branch: claude/pm-dispatch-objectos-ju9td1, the session's one designated push branch.

    No change to source.config.ts or next.config.mjs unless the fix requires it, stated in the report. No fumadocs-mdx patch unless it is the only fix.
    Stop on breach and explain in the report.
    Container & model: S, mode:subagent, model: default.
    Clause-②: no
    Thread-read: none
    Serial constraints cleared: no. Pushes are serial on the one branch: this card → #305 → #307 → #308.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 311,
      "status": "staged",
      "branch": "staged/issue-311",
      "worktree": "/home/user/objectos-issue-311",
      "base": "5d2f83770323b979bf65453e9c37875235ced9a3",
      "final_commit": "4afa327f2941e07e13efa23ddff6427d381c17cc",
      "commits": ["3f1b94f fix(docs): run the awaited fumadocs-mdx write after next typegen in type-check", "4afa327 docs(agents): record the type-check order and why it is load-bearing"],
      "pr": null,
      "session": "session_01FeA1nwBz1ohH65dvffUGKr",
      "premise_still_valid": true,
      "files_changed": ["apps/docs/package.json (type-check script order)", "AGENTS.md (Commands line, 1 line)"],
      "summary": "type-check is now `next typegen && fumadocs-mdx && tsc --noEmit`, so the awaited fumadocs-mdx CLI is the last writer of .source before tsc. The card's root cause checks out against the installed packages: fumadocs-mdx 15.0.7 dist/next/index.js:14-20 (init not awaited; :84-88 and :120 end in emit write), dist/core-DlDe_Eze.js:232-236 (fs.writeFile), next 16.2.6 dist/bin/next:170-172 (typegen then process.exit(0)), and fumadocs-mdx dist/bin.js:4-11 (the CLI awaits postInstall and never loads next.config.mjs). typegen does not read .source: with .source deleted it exits 0. No change to source.config.ts, next.config.mjs or ci.yml, and no fumadocs patch.",
      "red_green": {
        "harness": "scratchpad/issue-311/harness/slow-source-write.cjs, a throwaway NODE_OPTIONS=--require preload (not committed, not in node_modules). It makes each .source/*.ts write truncate at once and land after FD_RACE_DELAY_MS (fixed) or a random 0..FD_RACE_JITTER_MS. Runner: harness/run.sh. Logs: runs/.",
        "red_old_order_5d2f837": "fixed 3000ms: 3/3 red, TS2306 `lib/source.ts(1,22): error TS2306: File '.../apps/docs/.source/server.ts' is not a module.`, one [MDX] line instead of two, server.ts 0 bytes, harness `EXIT code=0 with 3 .source write(s) still pending`. Random 0-1500ms: 20/20 red, TS2306 in all 20. Natural, no delay, cold .source: 0/25 red (it did not fire locally; CI rate was 1 in 40).",
        "green_new_order": "On tree e06625d, the final tree (the sha rewrite changed only the commit message). Fixed 3000ms: 3/3 green. Random 0-1500ms: 20/20 green, and typegen still exited with writes pending in all 20, so the race fired and was absorbed. Natural, cold .source: 20/20 green."
      },
      "exposure": {
        "postinstall": "not exposed: the CLI awaits its writes; under the 3000ms harness it took 3052ms and left server.ts at 99755 bytes",
        "next_build": "not the same race: the process outlives the write, so no truncated file is left behind. It does have an in-process window where Turbopack reads .source/server.ts while createMDX's rewrite is running. Harness: passed at 50ms and 200ms, failed at 1000ms and 5000ms with `Export docs doesn't exist in target module`, a loud build error and not a bad artifact. In a natural build, [MDX] prints before `Creating an optimized production build`. Not fixed here; see open_questions",
        "next_dev": "not the same race: the process is long-lived and init(dev=true) goes on to start the watcher. Probe with a 20000ms delay: a request sent while the files were truncated returned 200 after a 104s first compile, with no error logged. Server stopped by its process group",
        "preview_deploy": "they run next build through opennextjs-cloudflare, so next_build applies; CI packages with --skipNextBuild"
      },
      "tests": "Union on HEAD 4afa327 (gates/final/, os-verify-lock VERDICT command-exit 0): gen-zh-hant --check exit 0 `✓ zh-Hant: 60 generated file(s) match the zh-Hans sources byte for byte.`; pnpm turbo run type-check --continue --force exit 0 `Tasks: 1 successful, 1 total`; NEXT_PRIVATE_STANDALONE=true pnpm turbo run build --force exit 0 `Tasks: 1 successful, 1 total`; pnpm turbo run test --force exit 0 `Tasks: 1 successful, 1 total` (`✓ 10 self-test(s) passed`); check-translation-ownership.mjs --actor hotlong --files changed.txt (git diff --name-status --no-renames BASE...HEAD) exit 0, and with TRANSLATION_BOT_LOGIN set exit 0 `✓ 2 file(s) changed, no translation artifacts touched.`; check-node-floor --self-test and check both exit 0, because the diff touches apps/docs/package.json, which it reads. Left to CI: Locale surface, Positioning, Search answers every locale (no content or route change).",
      "deviations": [
        "AGENTS.md is outside the claimed file surface. The diff made its Commands line false (it documented the old order), so I fixed the line and recorded why the order matters. It is in a separate commit, 4afa327, which the seat can drop.",
        "My first deterministic harness run was a no-op. Its regex expected a leading separator, but fumadocs writes the relative path .source/x.ts. I caught it because the harness printed nothing, fixed it, and re-ran; the no-op logs are in runs/base-delay3000-NOOP-regex.",
        "I first wrote the commit trailers with the model name copied from the harness reminder, which the agent contract does not allow. The commits were local and never pushed, so I rewrote the messages with commit-tree to `Co-Authored-By: Claude` plus Claude-Session. The tree is unchanged (e06625d) and the gate union was re-run on the new HEAD.",
        "The first type-check gate run was wrapped with `;`, so os-verify-lock printed batch-last-exit. It was superseded by the final union, which got command-exit 0.",
        "The build log shows `Failed to load dynamic font ... self-signed certificate in certificate chain`: the sandbox's TLS proxy blocks OG font fetches. The build still exits 0.",
        "The commands use objectstack's os-verify-lock.sh, because objectos has none."
      ],
      "pr_body_draft": "/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-311/pr-body.md (first line `Fixes #311`)",
      "mcp_calls": "0",
      "api_writes": "1 — POST repos/objectstack-ai/objectos/issues/311/comments (this report)",
      "open_questions": [
        {
          "question": "Close the next build in-process window too?",
          "options": ["A: leave it, state it as residual, and file an upstream report (createMDX should await init, or emit should write to a temp file and rename)", "B: build = `fumadocs-mdx && _FUMADOCS_MDX=1 next build`, which skips createMDX's rewrite through fumadocs-mdx's private recursion guard; if upstream renames it, it silently stops working and we are back at A", "C: pnpm-patch fumadocs-mdx to await init"],
          "recommendation": "A. The failure is loud. It was reproduced only with an injected delay of 1000ms or more, and no natural build failure is on record. B depends on a private variable and C goes against the card's no-patch rule. Revisit B if a natural build red appears."
        }
      ],
      "out_of_scope_findings": ["carrier: none · the next build in-process window (harness red at 1000ms and above) is noted in the PR Acceptance notes and open_questions, not filed · dedupe words: fumadocs-mdx createMDX init not awaited, .source truncated, Export docs doesn't exist, next build race"]
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #313 on head 5e666c36a842c906e423ab3a2260ee5792757a23 · 2026-10-06T15:35Z. I am landing it now: it changes one line, touches no governed path, and every check is green.

    Path surface (get_files): apps/docs/package.json, +1/−1. Only type-check changes, from fumadocs-mdx && next typegen && tsc --noEmit to next typegen && fumadocs-mdx && tsc --noEmit.

    Checks on 5e666c3: build and Node floor succeeded; Deploy docs was skipped. mergeable_state is clean on base 5d2f837.

    Push: fast-forward with no force. ff/issue-311 starts at the remote branch head 911a174, merges origin/main (its diff against main is then empty), and cherry-picks 3f1b94f. The PR's diff is byte-identical to the dev's git diff origin/main 3f1b94f.

    Review:

    • Root cause. I re-read it against the installed fumadocs-mdx 15.0.7 and agree. createMDX() does not await init (dist/next/index.js:14-20), emit truncates with fs.writeFile (dist/core-DlDe_Eze.js:232-236), and next typegen ends in process.exit(0). Moving the awaited CLI after typegen makes it the last writer before tsc.
    • Evidence. A delay harness reproduces the race: the old order fails 3/3 at a fixed 3000 ms and 20/20 at random 0–1500 ms, all with TS2306. The new order passes 3/3, 20/20, and 20/20 on a cold .source. In every green jitter run the race still fired and was absorbed.
    • Other entry points. postinstall and next dev are not exposed. next build keeps an in-process window that fails loudly, and only under a delay of 1000 ms or more. I accept option A, leaving it as a known residual: no reliance on fumadocs-mdx's private _FUMADOCS_MDX guard and no patch.

    Deviation handled by the seat: the dev's second commit (4afa327) updates the Commands line of AGENTS.md, which records the old order. AGENTS.md is governed and human-merge only, so it is not in this PR. The line stays staged and goes into the seat's pending governed AGENTS.md change, together with #256 part 2, so this fix does not wait on a maintainer merge.

    After merge: watch the main run, whose type-check now uses this order, through to the deploy verdict and smoke, then post the landed note here. #305 is next on the branch.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed and live · 2026-10-06T15:43Z

    • PR fix(docs): run the awaited fumadocs-mdx write after next typegen in type-check #313: squash 92a12d2, merged by the seat. Fixes #311 closed this card.
    • CI on main (run 37489001205, attempt 1): build succeeded first time with the new type-check order, along with Node floor, Deploy docs / Publish the tested Worker and Deploy docs / Smoke-check the live site. Roll back was skipped.
    • Deploy verdict: ✓ deploy: 17a1012c-a7c4-46fa-9f30-a7acd1112c2d is serving, replacing 12315e65-a391-4fd3-9d8f-a7e1b1e09a1c.
    • Still open:
      • The AGENTS.md Commands line goes with the seat's pending governed AGENTS.md change.
      • The next build in-process window is accepted as a known residual (option A).
    • Housekeeping: the seat removes pm:dispatched from this card.

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions