Skip to content

fix(build): 修复静态路径构建回归#4046

Closed
88lin wants to merge 1 commit into
notionnext-org:mainfrom
88lin:fix/pr4033-build-regressions
Closed

fix(build): 修复静态路径构建回归#4046
88lin wants to merge 1 commit into
notionnext-org:mainfrom
88lin:fix/pr4033-build-regressions

Conversation

@88lin
Copy link
Copy Markdown
Member

@88lin 88lin commented May 14, 2026

背景

PR #4033 将多个路由中的 getStaticPaths 逻辑抽到 getStaticPathsBase,用于复用静态路径生成和构建缓存逻辑。合并后,三个 prefix/slug 路由文件的 getStaticProps 仍然会调用 isExport() 来决定是否设置 ISR revalidate,但对应的 isExport 导入被移除,导致构建预渲染阶段直接失败。

相关部署报错见:

典型错误为:

ReferenceError: isExport is not defined
Error occurred prerendering page ...
Command "yarn run build" exited with 1

该问题会影响 Vercel、Cloudflare Pages、本地 yarn build / yarn export 等构建链路。

修复内容

本 PR 在保留 #4033 构建优化方向的基础上,补齐合并后遗漏的构建稳定性修复:

  • 恢复三个 prefix/slug 路由文件对 isExport helper 的导入:
    • pages/[prefix]/index.js
    • pages/[prefix]/[slug]/index.js
    • pages/[prefix]/[slug]/[...suffix].js
  • 继续统一使用 @/lib/utils/buildMode 中的 isExport(),避免在多个路由文件中重复内联导出模式判断逻辑。
  • 保持 getStaticPathsBase 负责统一处理 export / ISR 两种构建路径,减少路由文件里的重复逻辑。
  • 修复 getSharedAllPages 对失败 promise 的进程内缓存问题:当 Notion 数据拉取失败时清理对应缓存 key,避免后续请求一直复用同一个 rejected promise。
  • 统一 staticPaths 缓存 key 的 pageId 默认值,使其与 fetchGlobalAllData 的默认行为保持一致。
  • 先清理 tagOptions,再用清理后的标签集合过滤页面 tagItems,避免页面保留已被过滤标签导致标签页跳转 404。

修复效果

验证

已在修复分支执行以下验证:

  • yarn install --frozen-lockfile
  • yarn test __tests__/lib/staticPaths.test.js --runInBand
  • yarn test --runInBand
  • ./node_modules/.bin/eslint pages/[prefix]/index.js pages/[prefix]/[slug]/index.js pages/[prefix]/[slug]/[...suffix].js lib/build/staticPaths.js lib/db/SiteDataApi.js __tests__/lib/staticPaths.test.js
  • yarn type-check
  • yarn build
  • yarn export

验证结果:

  • yarn buildyarn export 均已通过。
  • 未再出现 ReferenceError: isExport is not defined
  • 静态路径相关单测和全量测试均已通过。

- 恢复三个 prefix/slug 路由对 isExport helper 的导入,避免 getStaticProps 在预渲染时触发 ReferenceError
- 清理 staticPaths 中失败的进程内 allPages promise,避免一次 Notion 拉取失败后持续复用 rejected promise
- 统一 staticPaths 缓存 key 的 pageId 默认值,减少未来直接调用时的缓存分歧
- 先清理 tagOptions 再过滤页面 tagItems,避免保留已过滤标签导致标签页 404

验证:
- yarn test --runInBand
- yarn type-check
- eslint 本次改动文件(0 errors,pages/[prefix]/index.js 保留既有 hook warnings)
- yarn build(第一次受 Notion ECONNRESET 影响失败,第二次通过)
- yarn export

说明:
- 未调整 fetchGlobalAllData 的 deepClone 顺序;当前上游仍是先克隆再清理,不会污染 site_ 缓存
- 未移除 build_static_paths_all_pages 缓存;它服务于 notionnext-org#4033 的跨 worker 构建复用,保留更稳
@88lin
Copy link
Copy Markdown
Member Author

88lin commented May 14, 2026

🔍 PR #4046 审查结论:


✅ 核心修复:isExport 导入恢复 —— 完全正确

三个路由文件各加了一行 import { isExport } from '@/lib/utils/buildMode'精准解决了构建崩溃的根因

文件 修复
pages/[prefix]/index.js +1行,恢复 isExport 导入
pages/[prefix]/[slug]/index.js +1行,恢复 isExport 导入
pages/[prefix]/[slug]/[...suffix].js +1行,恢复 isExport 导入

这和之前我们分析出的问题完全吻合,修法也是最直接正确的。


✅ 额外修复 1:rejected promise 缓存泄漏 —— 修得很好

// 修复前:失败后 rejected promise 永远留在 Map 里,后续请求全部失败
inProcessAllPagesPromises.set(cacheKey, promise)

// 修复后:失败时自动清理,下次请求可以重试
promise.catch(() => {
  inProcessAllPagesPromises.delete(cacheKey)
})
inProcessAllPagesPromises.set(cacheKey, promise)

没有竞态条件问题

  • .catch() 在 JS 中无论 promise 是否已 rejected 都会触发(微任务机制保证)
  • .catch() 只做 Map 清理,不影响调用方对原始 promise 的 await
  • 新增了专门的测试用例验证此行为

✅ 额外修复 2:缓存键默认值对齐 —— 修得到位

// 修复前:pageId 为 null 时,staticPaths 用 'default',SiteDataApi 用 BLOG.NOTION_PAGE_ID
const safePageId = String(pageId || 'default').replace(...)

// 修复后:两处统一使用 BLOG.NOTION_PAGE_ID
const safePageId = String(pageId || BLOG.NOTION_PAGE_ID).replace(...)

这个 bug 虽然在当前正常调用路径下不会触发(因为参数有默认值),但防御性地修正确是正确的做法。


✅ 额外修复 3:tag 清理顺序 —— 发现了一个真实的用户侧 Bug

// 修复前(错误顺序):
db.allPages = cleanPages(db.allPages, db.tagOptions)   // ← 用的是未清理的 tagOptions
db.tagOptions = cleanTagOptions(db?.tagOptions)          // ← 清理太晚了

// 修复后(正确顺序):
db.tagOptions = cleanTagOptions(db?.tagOptions)          // ← 先清理 tagOptions
db.allPages = cleanPages(db.allPages, db.tagOptions)     // ← 再用清理后的 tagOptions 过滤页面

这是一个真实影响用户的 bug:旧的顺序会导致页面的 tagItems 保留对已删除标签的引用,用户点击标签后出现 404。这个 bug 和 PR #4033 无关,但被这个修复 PR 顺带发现了,修得很好。


📊 与竞品 PR #4044 的对比

对比项 PR #4044 PR #4046(88lin)
isExport 修复方式 内联 process.env.EXPORT === 'true' 恢复 import { isExport }
rejected promise 泄漏 ❌ 没修 ✅ 修了
缓存键默认值不一致 ❌ 没修 ✅ 修了
tag 清理顺序 bug ❌ 没修 ✅ 修了
测试覆盖 ❌ 没加 ✅ 新增 1 个测试
改动量 +9/-3(3文件) +39/-2(6文件)

结论:PR #4046 明显优于 PR #4044 PR #4044 只做了最小修复(甚至修法不太规范——用内联变量替代共享函数),而 88lin 的 PR #4046 不仅修了崩溃问题,还一并修复了 3 个潜在/实际 bug,且有测试覆盖。


🏁 最终结论

评估项 结论
能否解决构建报错? 可以isExport 导入恢复后 ReferenceError 消失
修复质量 ✅ 高质量,还额外修了 3 个 bug
是否引入新问题? ❌ 没有发现
CI 状态 CodeQL ✅ / Build 🔄 进行中
建议 推荐合并,等 CI 通过后即可合入

tangly1024 added a commit that referenced this pull request May 15, 2026
…4046)

- Align staticPaths cache key default pageId with fetchGlobalAllData (BLOG.NOTION_PAGE_ID)
- Clear in-process allPages promise entry on rejection so builds can retry
- Run cleanTagOptions before cleanPages to avoid stale tagItems -> 404
- Add regression test for rejected then successful getSharedAllPages

Implements #4046; relates to #4043.

Co-authored-by: Cursor <cursoragent@cursor.com>
@tangly1024
Copy link
Copy Markdown
Collaborator

已在 main 以提交 5700f36 落地与本 PR 等价的改动(staticPaths 缓存 key、rejected promise 清理、handleDataBeforeReturn 中先 cleanTagOptionscleanPages、以及对应单测)。与当前 main 上已有的 isExport 导入修复(62a7c580)组合后,构建与 #4043 描述的问题应对齐。

由于 main 已前进且与本分支存在合并冲突,无法再通过 GitHub 无冲突 squash 合并本 PR;为避免重复提交,关闭本 PR。感谢 @88lin 的完整分析与验证说明。

若需保留 PR 记录为 merged,可由贡献者将分支 rebase 到最新 main 后推送空提交或关闭本 PR 另开「cherry-pick 文档」类 PR(通常不必)。

@tangly1024
Copy link
Copy Markdown
Collaborator

Superseded: equivalent changes merged to main in 5700f36 (see PR comment).

@tangly1024 tangly1024 closed this May 15, 2026
@88lin 88lin deleted the fix/pr4033-build-regressions branch May 17, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

【Next.js 构建报错】ReferenceError: isExport is not defined

2 participants