@jant/core 0.6.14 → 0.6.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/bin/commands/import-site.js +88 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-C75Zc8dD.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
  13. package/dist/client/.vite/manifest.json +8 -8
  14. package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
  16. package/dist/client/_assets/client-D3VZyG4L.css +2 -0
  17. package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
  18. package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
  19. package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
  20. package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
  21. package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
  22. package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
  23. package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
  24. package/dist/index.js +5 -5
  25. package/dist/node.js +7 -7
  26. package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
  27. package/package.json +11 -10
  28. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  29. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  30. package/src/__tests__/export-hugo-build.test.ts +63 -4
  31. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  32. package/src/__tests__/export-service.test.ts +142 -9
  33. package/src/__tests__/helpers/app.ts +3 -0
  34. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  35. package/src/__tests__/import-site-command.test.ts +65 -0
  36. package/src/__tests__/mise-config.test.ts +22 -0
  37. package/src/__tests__/vite-config.test.ts +45 -0
  38. package/src/app.tsx +16 -3
  39. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  40. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  41. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  42. package/src/client/__tests__/compose-bridge.test.ts +316 -2
  43. package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
  44. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  45. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  46. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  47. package/src/client/__tests__/toast.test.ts +82 -1
  48. package/src/client/collection-created-notice.ts +66 -0
  49. package/src/client/collection-form-bridge.ts +17 -0
  50. package/src/client/collection-navigation.ts +44 -0
  51. package/src/client/collection-page-actions.ts +57 -1
  52. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  53. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  54. package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
  55. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
  56. package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
  57. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
  58. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  59. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  60. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  61. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  62. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  63. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  64. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  65. package/src/client/components/collection-manager-types.ts +9 -3
  66. package/src/client/components/compose-format-convert.ts +5 -1
  67. package/src/client/components/compose-types.ts +55 -3
  68. package/src/client/components/jant-collection-directory.ts +165 -30
  69. package/src/client/components/jant-command-palette.ts +14 -7
  70. package/src/client/components/jant-compose-dialog.ts +1542 -694
  71. package/src/client/components/jant-compose-editor.ts +237 -99
  72. package/src/client/components/jant-compose-fullscreen.ts +27 -9
  73. package/src/client/components/jant-config-editor.ts +722 -0
  74. package/src/client/components/jant-media-lightbox.ts +100 -18
  75. package/src/client/components/jant-nav-manager.ts +1283 -54
  76. package/src/client/components/jant-post-form.ts +3 -3
  77. package/src/client/components/jant-post-menu.ts +177 -95
  78. package/src/client/components/nav-manager-types.ts +51 -2
  79. package/src/client/components/post-form-types.ts +1 -1
  80. package/src/client/compose-bridge.ts +147 -123
  81. package/src/client/compose-launch.ts +13 -0
  82. package/src/client/compose-shortcuts.ts +5 -1
  83. package/src/client/feed-video-player.ts +62 -2
  84. package/src/client/footnote-rail.ts +478 -0
  85. package/src/client/hydrate-partial.ts +2 -0
  86. package/src/client/media-lightbox-events.ts +63 -0
  87. package/src/client/post-refresh.ts +135 -0
  88. package/src/client/site-header-fragment.ts +38 -0
  89. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  90. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  91. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  92. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  93. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  94. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  95. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  96. package/src/client/tiptap/bubble-menu.ts +88 -10
  97. package/src/client/tiptap/continuous-lists.ts +102 -0
  98. package/src/client/tiptap/create-editor.ts +3 -0
  99. package/src/client/tiptap/extensions.ts +14 -5
  100. package/src/client/tiptap/link-input-rules.ts +4 -4
  101. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  102. package/src/client/tiptap/slash-commands.ts +8 -1
  103. package/src/client/tiptap/structural-keymap.ts +211 -0
  104. package/src/client/tiptap/tab-indent.ts +8 -22
  105. package/src/client/tiptap/table-control-labels.ts +30 -0
  106. package/src/client/tiptap/table-controls.ts +537 -0
  107. package/src/client/tiptap/table-size-picker.ts +254 -0
  108. package/src/client/toast.ts +133 -39
  109. package/src/client-auth.ts +1 -0
  110. package/src/client.ts +1 -0
  111. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  112. package/src/db/__tests__/d1-query.test.ts +43 -1
  113. package/src/db/__tests__/migration-runner.test.ts +63 -0
  114. package/src/db/__tests__/migrations.test.ts +469 -0
  115. package/src/db/__tests__/thread-activity.test.ts +113 -0
  116. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  117. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  118. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  119. package/src/db/migrations/0028_superb_post.sql +2 -0
  120. package/src/db/migrations/0029_boring_magma.sql +1 -0
  121. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  122. package/src/db/migrations/0031_many_ego.sql +3 -0
  123. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  124. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  125. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  126. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  127. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  128. package/src/db/migrations/meta/_journal.json +35 -0
  129. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  130. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  131. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  132. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  133. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  134. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  135. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  136. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  137. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  138. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  139. package/src/db/migrations/pg/meta/_journal.json +35 -0
  140. package/src/db/pg/schema.ts +46 -20
  141. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  142. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  143. package/src/db/schema.ts +48 -20
  144. package/src/db/thread-activity.ts +101 -0
  145. package/src/i18n/__tests__/detect.test.ts +8 -2
  146. package/src/i18n/detect.ts +14 -5
  147. package/src/i18n/locales/public/en.po +222 -51
  148. package/src/i18n/locales/public/en.ts +1 -1
  149. package/src/i18n/locales/public/zh-Hans.po +216 -45
  150. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  151. package/src/i18n/locales/public/zh-Hant.po +216 -45
  152. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  153. package/src/i18n/locales/settings/en.po +513 -10
  154. package/src/i18n/locales/settings/en.ts +1 -1
  155. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  156. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  157. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  158. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  159. package/src/lib/__tests__/api-settings.test.ts +299 -0
  160. package/src/lib/__tests__/constants.test.ts +5 -0
  161. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  162. package/src/lib/__tests__/feed.test.ts +168 -0
  163. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  164. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  165. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  166. package/src/lib/__tests__/markdown.test.ts +24 -6
  167. package/src/lib/__tests__/navigation.test.ts +59 -2
  168. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  169. package/src/lib/__tests__/post-display.test.ts +64 -0
  170. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  171. package/src/lib/__tests__/slug.test.ts +18 -0
  172. package/src/lib/__tests__/summary.test.ts +82 -0
  173. package/src/lib/__tests__/timeline.test.ts +109 -70
  174. package/src/lib/__tests__/timezones.test.ts +31 -3
  175. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  176. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  177. package/src/lib/__tests__/translit.test.ts +87 -0
  178. package/src/lib/__tests__/url.test.ts +53 -0
  179. package/src/lib/__tests__/view.test.ts +53 -0
  180. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  181. package/src/lib/api-settings.ts +239 -12
  182. package/src/lib/collection-sort.ts +7 -5
  183. package/src/lib/constants.ts +2 -0
  184. package/src/lib/feed-path.ts +25 -0
  185. package/src/lib/feed-policy.ts +64 -0
  186. package/src/lib/feed.ts +113 -11
  187. package/src/lib/footnotes.ts +264 -0
  188. package/src/lib/github-sync-site-config.ts +2 -0
  189. package/src/lib/hugo-markdown.ts +15 -3
  190. package/src/lib/jant-branding.ts +1 -0
  191. package/src/lib/link-preview.ts +25 -0
  192. package/src/lib/markdown-manager.ts +193 -5
  193. package/src/lib/markdown.ts +10 -3
  194. package/src/lib/navigation.ts +16 -3
  195. package/src/lib/post-body-html.ts +131 -0
  196. package/src/lib/post-display.ts +47 -8
  197. package/src/lib/render.tsx +9 -2
  198. package/src/lib/resolve-config.ts +100 -16
  199. package/src/lib/schemas.ts +133 -1
  200. package/src/lib/settings-paths.ts +5 -0
  201. package/src/lib/site-skill.ts +78 -0
  202. package/src/lib/summary.ts +16 -5
  203. package/src/lib/timeline.ts +137 -131
  204. package/src/lib/timezones.ts +54 -14
  205. package/src/lib/tiptap-render.ts +359 -83
  206. package/src/lib/translit.ts +288 -0
  207. package/src/lib/url.ts +123 -12
  208. package/src/lib/view.ts +58 -20
  209. package/src/lib/worker-response-cache.ts +11 -18
  210. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  211. package/src/middleware/config.ts +2 -2
  212. package/src/middleware/public-content-access.ts +87 -0
  213. package/src/node/__tests__/cli-export.test.ts +14 -0
  214. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  215. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  216. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  217. package/src/node/index.ts +2 -1
  218. package/src/preset.css +16 -2
  219. package/src/routes/__tests__/compose.test.ts +51 -0
  220. package/src/routes/__tests__/site-skill.test.ts +112 -0
  221. package/src/routes/api/__tests__/collections.test.ts +91 -19
  222. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  223. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  224. package/src/routes/api/__tests__/palette.test.ts +44 -0
  225. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  226. package/src/routes/api/__tests__/settings.test.ts +217 -0
  227. package/src/routes/api/collections.ts +22 -26
  228. package/src/routes/api/export.ts +2 -0
  229. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  230. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  231. package/src/routes/api/internal/post-body-html.ts +43 -0
  232. package/src/routes/api/internal/sites.ts +24 -0
  233. package/src/routes/api/nav-items.ts +24 -1
  234. package/src/routes/api/posts.ts +14 -10
  235. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  236. package/src/routes/api/public/__tests__/posts.test.ts +246 -0
  237. package/src/routes/api/public/archive.ts +4 -0
  238. package/src/routes/api/public/posts.ts +48 -24
  239. package/src/routes/api/search.ts +3 -0
  240. package/src/routes/api/settings.ts +33 -0
  241. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  242. package/src/routes/compose.tsx +8 -3
  243. package/src/routes/dash/settings.tsx +45 -2
  244. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  245. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  246. package/src/routes/feed/feed.ts +23 -14
  247. package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
  248. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  249. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  250. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  251. package/src/routes/pages/__tests__/preview.test.ts +176 -0
  252. package/src/routes/pages/__tests__/search.test.ts +69 -0
  253. package/src/routes/pages/archive.tsx +90 -22
  254. package/src/routes/pages/collection.tsx +32 -19
  255. package/src/routes/pages/collections.tsx +6 -1
  256. package/src/routes/pages/page.tsx +86 -11
  257. package/src/routes/site-skill.ts +18 -0
  258. package/src/services/__tests__/auth.test.ts +7 -5
  259. package/src/services/__tests__/collection.test.ts +190 -44
  260. package/src/services/__tests__/navigation.test.ts +229 -21
  261. package/src/services/__tests__/post-timeline.test.ts +249 -39
  262. package/src/services/__tests__/post.test.ts +844 -5
  263. package/src/services/__tests__/search.test.ts +48 -1
  264. package/src/services/__tests__/settings.test.ts +8 -0
  265. package/src/services/__tests__/site-admin.test.ts +121 -1
  266. package/src/services/auth.ts +2 -2
  267. package/src/services/bootstrap.ts +3 -2
  268. package/src/services/collection.ts +224 -167
  269. package/src/services/export-theme/assets/client-site.css +1 -1
  270. package/src/services/export-theme/assets/client-site.js +46 -25
  271. package/src/services/export-theme/layouts/_default/list.html +18 -4
  272. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  273. package/src/services/export-theme/layouts/featured/list.html +5 -4
  274. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  275. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  276. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  277. package/src/services/export-theme/styles/main.css +30 -2
  278. package/src/services/export.ts +94 -85
  279. package/src/services/mcp.ts +37 -27
  280. package/src/services/media.ts +1 -1
  281. package/src/services/navigation.ts +185 -46
  282. package/src/services/path.ts +4 -1
  283. package/src/services/post.ts +1142 -342
  284. package/src/services/search.ts +17 -2
  285. package/src/services/settings.ts +2 -10
  286. package/src/services/site-admin.ts +63 -43
  287. package/src/style-cjk-jp.css +1 -1
  288. package/src/style-cjk-kr.css +1 -1
  289. package/src/style-cjk-tc.css +1 -1
  290. package/src/style-cjk.css +1 -1
  291. package/src/styles/components.css +540 -14
  292. package/src/styles/site-media.css +111 -19
  293. package/src/styles/tokens.css +76 -10
  294. package/src/styles/ui.css +2227 -1347
  295. package/src/types/bindings.ts +3 -0
  296. package/src/types/config.ts +243 -12
  297. package/src/types/constants.ts +22 -1
  298. package/src/types/entities.ts +14 -8
  299. package/src/types/operations.ts +18 -10
  300. package/src/types/props.ts +20 -2
  301. package/src/types/raw-assets.d.ts +5 -0
  302. package/src/types/views.ts +22 -4
  303. package/src/ui/__tests__/font-themes.test.ts +42 -30
  304. package/src/ui/compose/ComposeDialog.tsx +109 -19
  305. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  306. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  307. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  308. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  309. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  310. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  311. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  312. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  313. package/src/ui/feed/LinkCard.tsx +2 -12
  314. package/src/ui/feed/LinkPreview.tsx +2 -7
  315. package/src/ui/feed/NoteCard.tsx +10 -20
  316. package/src/ui/feed/PostStatusBadges.tsx +42 -3
  317. package/src/ui/feed/QuoteCard.tsx +2 -12
  318. package/src/ui/feed/ThreadPreview.tsx +26 -34
  319. package/src/ui/feed/TimelineFeed.tsx +2 -2
  320. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  321. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  322. package/src/ui/feed/thread-preview-state.ts +5 -7
  323. package/src/ui/font-themes.ts +74 -26
  324. package/src/ui/layouts/BaseLayout.tsx +22 -8
  325. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  326. package/src/ui/pages/ArchivePage.tsx +131 -24
  327. package/src/ui/pages/CollectionPage.tsx +101 -22
  328. package/src/ui/pages/CollectionsPage.tsx +3 -1
  329. package/src/ui/pages/PostPage.tsx +39 -12
  330. package/src/ui/pages/SearchPage.tsx +6 -14
  331. package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
  332. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  333. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  334. package/src/ui/shared/CollectionsManager.tsx +17 -7
  335. package/src/ui/shared/DraftPreviewBar.tsx +49 -0
  336. package/src/ui/shared/HomePageBranding.tsx +2 -2
  337. package/src/ui/shared/MediaGallery.tsx +1 -0
  338. package/src/ui/shared/PostFooter.tsx +38 -13
  339. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  340. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  341. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  342. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  343. package/src/ui/shared/collection-management-labels.ts +33 -2
  344. package/src/ui/shared/custom-icons.ts +5 -0
  345. package/src/ui/shared/navigation-labels.ts +2 -4
  346. package/src/ui/shared/post-article-attributes.ts +52 -0
  347. package/dist/app-DZDlmh7C.js +0 -6
  348. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  349. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  350. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -840,6 +840,20 @@ async function loadSiteConfig(rootDir) {
840
840
  params.show_header_avatar ?? jantData.show_header_avatar,
841
841
  ),
842
842
  noindex: coerceBoolean(params.noindex ?? jantData.noindex),
843
+ public_api_enabled:
844
+ params.public_api_enabled === undefined &&
845
+ jantData.public_api_enabled === undefined
846
+ ? true
847
+ : coerceBoolean(
848
+ params.public_api_enabled ?? jantData.public_api_enabled,
849
+ ),
850
+ rss_feeds_enabled:
851
+ params.rss_feeds_enabled === undefined &&
852
+ jantData.rss_feeds_enabled === undefined
853
+ ? true
854
+ : coerceBoolean(
855
+ params.rss_feeds_enabled ?? jantData.rss_feeds_enabled,
856
+ ),
843
857
  site_footer_markdown:
844
858
  typeof jantData.site_footer_markdown === "string"
845
859
  ? jantData.site_footer_markdown
@@ -896,6 +910,8 @@ function buildSettingsUpdatesFromConfig(siteConfig, customCss = "") {
896
910
  SITE_FOOTER: String(jant.site_footer_markdown || ""),
897
911
  SHOW_JANT_BRANDING_ON_HOME: jant.show_jant_branding_on_home ? "true" : "",
898
912
  NOINDEX: jant.noindex ? "true" : "",
913
+ PUBLIC_API_ENABLED: jant.public_api_enabled === false ? "false" : "true",
914
+ RSS_FEEDS_ENABLED: jant.rss_feeds_enabled === false ? "false" : "true",
899
915
  SHOW_HEADER_AVATAR: jant.show_header_avatar ? "true" : "",
900
916
  THEME: themeId && themeId !== defaultThemeId ? themeId : "",
901
917
  FONT_THEME: fontThemeId && fontThemeId !== "default" ? fontThemeId : "",
@@ -1615,7 +1631,7 @@ async function walkHugoContent(rootDir) {
1615
1631
  }
1616
1632
 
1617
1633
  /**
1618
- * Resolve the collection memberships listed in a bundle's top-level
1634
+ * Resolve the collection memberships listed in a post bundle's top-level
1619
1635
  * `collections:` front-matter array into `{collectionId, createdAt,
1620
1636
  * position, pinnedAt}` records the post-create APIs expect.
1621
1637
  *
@@ -1634,14 +1650,16 @@ function resolveCollectionMemberships(frontMatter, collectionSlugToId) {
1634
1650
  const id = collectionSlugToId.get(raw.slug);
1635
1651
  if (!id) continue;
1636
1652
  const entry = { collectionId: id };
1637
- if (typeof raw.collected_at === "string" && raw.collected_at) {
1638
- entry.createdAt = Math.floor(new Date(raw.collected_at).getTime() / 1000);
1653
+ const createdAt = parseImportTimestamp(raw.collected_at);
1654
+ if (createdAt !== null) {
1655
+ entry.createdAt = createdAt;
1639
1656
  }
1640
- if (typeof raw.position === "number") {
1657
+ if (typeof raw.position === "number" && Number.isFinite(raw.position)) {
1641
1658
  entry.position = raw.position;
1642
1659
  }
1643
- if (typeof raw.pinned_at === "string" && raw.pinned_at) {
1644
- entry.pinnedAt = Math.floor(new Date(raw.pinned_at).getTime() / 1000);
1660
+ const pinnedAt = parseImportTimestamp(raw.pinned_at);
1661
+ if (pinnedAt !== null) {
1662
+ entry.pinnedAt = pinnedAt;
1645
1663
  }
1646
1664
  entries.push(entry);
1647
1665
  ids.push(id);
@@ -1649,6 +1667,59 @@ function resolveCollectionMemberships(frontMatter, collectionSlugToId) {
1649
1667
  return { entries, ids };
1650
1668
  }
1651
1669
 
1670
+ /**
1671
+ * Resolve one Thread's collection memberships from its root and every reply.
1672
+ *
1673
+ * Current exports write `collections:` only on the root bundle. Older exports
1674
+ * wrote it per post, so import must take the union before creating the root.
1675
+ * Duplicate memberships retain all historical information by taking the
1676
+ * greatest collected/pinned timestamps and the smallest position.
1677
+ */
1678
+ function resolveThreadCollectionMemberships(rootBundle, collectionSlugToId) {
1679
+ const byCollectionId = new Map();
1680
+ const bundles = [rootBundle, ...(rootBundle.children ?? [])];
1681
+
1682
+ for (const bundle of bundles) {
1683
+ const { entries } = resolveCollectionMemberships(
1684
+ bundle.frontMatter,
1685
+ collectionSlugToId,
1686
+ );
1687
+
1688
+ for (const entry of entries) {
1689
+ const current = byCollectionId.get(entry.collectionId);
1690
+ if (!current) {
1691
+ byCollectionId.set(entry.collectionId, { ...entry });
1692
+ continue;
1693
+ }
1694
+
1695
+ if (entry.createdAt !== undefined) {
1696
+ current.createdAt =
1697
+ current.createdAt === undefined
1698
+ ? entry.createdAt
1699
+ : Math.max(current.createdAt, entry.createdAt);
1700
+ }
1701
+ if (entry.position !== undefined) {
1702
+ current.position =
1703
+ current.position === undefined
1704
+ ? entry.position
1705
+ : Math.min(current.position, entry.position);
1706
+ }
1707
+ if (entry.pinnedAt !== undefined) {
1708
+ current.pinnedAt =
1709
+ current.pinnedAt === undefined
1710
+ ? entry.pinnedAt
1711
+ : Math.max(current.pinnedAt, entry.pinnedAt);
1712
+ }
1713
+ }
1714
+ }
1715
+
1716
+ const entries = [...byCollectionId.values()];
1717
+ return {
1718
+ entries,
1719
+ ids: entries.map((entry) => entry.collectionId),
1720
+ };
1721
+ }
1722
+
1652
1723
  function parseImportTimestamp(value) {
1653
1724
  if (typeof value !== "string" || value.trim() === "") return null;
1654
1725
  const timestamp = Date.parse(value);
@@ -1672,6 +1743,13 @@ function getImportedPostStatus(frontMatter) {
1672
1743
  function shouldImportReplyQuietly(rootFrontMatter, replyFrontMatter) {
1673
1744
  if (getImportedPostStatus(replyFrontMatter) !== "published") return false;
1674
1745
 
1746
+ // Exports since quiet_reply became a stored column say so explicitly.
1747
+ if (typeof replyFrontMatter.quiet_reply === "boolean") {
1748
+ return replyFrontMatter.quiet_reply;
1749
+ }
1750
+
1751
+ // Older bundles: a reply published after the root's recorded last activity
1752
+ // can only have gotten there by skipping the bump, so it was quiet.
1675
1753
  const rootLastActivityAt = getImportedRootLastActivityAt(rootFrontMatter);
1676
1754
  const replyPublishedAt = parseImportTimestamp(replyFrontMatter.date);
1677
1755
  return (
@@ -1795,6 +1873,7 @@ export const __test__ = {
1795
1873
  walkHugoContent,
1796
1874
  mediaSpecFromJantMedia,
1797
1875
  resolveCollectionMemberships,
1876
+ resolveThreadCollectionMemberships,
1798
1877
  buildPostPayloadFromBundle,
1799
1878
  shouldImportReplyQuietly,
1800
1879
  };
@@ -2220,8 +2299,8 @@ export async function run(argv) {
2220
2299
  }
2221
2300
  }
2222
2301
 
2223
- const memberships = resolveCollectionMemberships(
2224
- rootFm,
2302
+ const memberships = resolveThreadCollectionMemberships(
2303
+ rootBundle,
2225
2304
  collectionSlugToId,
2226
2305
  );
2227
2306
  const postData = buildPostPayloadFromBundle(rootBundle, {
@@ -2357,14 +2436,10 @@ export async function run(argv) {
2357
2436
  if (textAttachment) replyAttachments.push(textAttachment);
2358
2437
  }
2359
2438
 
2360
- const replyMemberships = resolveCollectionMemberships(
2361
- replyFm,
2362
- collectionSlugToId,
2363
- );
2364
2439
  const replyData = buildPostPayloadFromBundle(replyBundle, {
2365
2440
  bodyMarkdown: replyBody,
2366
2441
  attachments: replyAttachments,
2367
- memberships: replyMemberships,
2442
+ memberships: { entries: [], ids: [] },
2368
2443
  replyToId: threadTailId,
2369
2444
  quietReply: shouldImportReplyQuietly(rootFm, replyFm),
2370
2445
  });
@@ -9,6 +9,7 @@ import {
9
9
  applyNodeBackfills,
10
10
  applyPgBackfills,
11
11
  } from "../lib/migration-runner.js";
12
+ import { queryD1 } from "../lib/d1-query.js";
12
13
  import { loadNodeRuntime } from "../lib/load-node-runtime.js";
13
14
  import { loadNodeEnvFile } from "../lib/node-env.js";
14
15
  import { openNodeSqlite, resolveDatabaseDialect } from "../lib/node-sqlite.js";
@@ -16,6 +17,10 @@ import {
16
17
  bootstrapCliRuntime,
17
18
  getCliRuntimeLabel,
18
19
  } from "../lib/runtime-target.js";
20
+ import {
21
+ preflightThreadCollectionMigration,
22
+ verifyThreadCollectionMigration,
23
+ } from "../lib/thread-collection-migration.js";
19
24
 
20
25
  export { loadNodeEnvFile };
21
26
 
@@ -167,6 +172,27 @@ async function logCliPgMigrationDebug(databaseUrl, phase) {
167
172
  }
168
173
  }
169
174
 
175
+ async function withNodeMigrationQuery(databaseDialect, databaseUrl, callback) {
176
+ if (databaseDialect === "pg") {
177
+ const pool = new Pool({ connectionString: databaseUrl });
178
+ try {
179
+ return await callback(async (sql) => (await pool.query(sql)).rows);
180
+ } finally {
181
+ await pool.end().catch(() => undefined);
182
+ }
183
+ }
184
+
185
+ const { sqlite } = openNodeSqlite(process.env, {
186
+ createParentDir: true,
187
+ requireInitialized: false,
188
+ });
189
+ try {
190
+ return await callback((sql) => sqlite.prepare(sql).all());
191
+ } finally {
192
+ sqlite.close();
193
+ }
194
+ }
195
+
170
196
  export async function run(argv) {
171
197
  const { values } = parseArgs({
172
198
  args: argv,
@@ -256,6 +282,16 @@ export async function run(argv) {
256
282
  }
257
283
 
258
284
  if (runtime === "node") {
285
+ const threadCollectionPreflight = await withNodeMigrationQuery(
286
+ databaseDialect,
287
+ databaseUrl,
288
+ (query) =>
289
+ preflightThreadCollectionMigration({
290
+ dialect: databaseDialect === "pg" ? "pg" : "sqlite",
291
+ query,
292
+ }),
293
+ );
294
+
259
295
  if (debugMigrate && databaseDialect === "pg") {
260
296
  await logCliPgMigrationDebug(databaseUrl, "before");
261
297
  }
@@ -270,6 +306,16 @@ export async function run(argv) {
270
306
  throw error;
271
307
  }
272
308
 
309
+ await withNodeMigrationQuery(databaseDialect, databaseUrl, (query) =>
310
+ verifyThreadCollectionMigration(
311
+ {
312
+ dialect: databaseDialect === "pg" ? "pg" : "sqlite",
313
+ query,
314
+ },
315
+ threadCollectionPreflight,
316
+ ),
317
+ );
318
+
273
319
  if (debugMigrate && databaseDialect === "pg") {
274
320
  await logCliPgMigrationDebug(databaseUrl, "after");
275
321
  }
@@ -299,7 +345,16 @@ export async function run(argv) {
299
345
  env: values.env,
300
346
  persistTo: values["persist-to"],
301
347
  };
348
+ const query = (sql) => queryD1(sql, runtime, options);
349
+ const threadCollectionPreflight = await preflightThreadCollectionMigration({
350
+ dialect: "sqlite",
351
+ query,
352
+ });
302
353
  applyD1SchemaMigrations(runtime, options);
354
+ await verifyThreadCollectionMigration(
355
+ { dialect: "sqlite", query },
356
+ threadCollectionPreflight,
357
+ );
303
358
  applyD1Backfills(runtime, options);
304
359
  }
305
360
 
@@ -0,0 +1,184 @@
1
+ import { parseArgs } from "node:util";
2
+ import { resolveSiteUrl } from "../../lib/site-url.js";
3
+
4
+ const INTERNAL_ADMIN_TOKEN_ENV_VAR = "INTERNAL_ADMIN_TOKEN";
5
+ const DEFAULT_BATCH_SIZE = 50;
6
+ const MAX_BATCH_SIZE = 100;
7
+
8
+ function normalizeBaseUrl(value) {
9
+ const trimmed = value.trim();
10
+ return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
11
+ }
12
+
13
+ function buildRebuildUrl(siteUrl, siteId) {
14
+ const path = siteId
15
+ ? `api/internal/sites/${encodeURIComponent(siteId)}/posts/body-html/rebuild`
16
+ : "api/internal/posts/body-html/rebuild";
17
+ return new URL(path, normalizeBaseUrl(siteUrl)).toString();
18
+ }
19
+
20
+ function parseBatchSize(rawLimit) {
21
+ if (rawLimit === undefined) return DEFAULT_BATCH_SIZE;
22
+
23
+ const limit = Number.parseInt(rawLimit, 10);
24
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_BATCH_SIZE) {
25
+ throw new Error(
26
+ `Batch size must be an integer between 1 and ${MAX_BATCH_SIZE}.`,
27
+ );
28
+ }
29
+ return limit;
30
+ }
31
+
32
+ async function requestBatch(url, token, { limit, cursor, dryRun }) {
33
+ const response = await fetch(url, {
34
+ method: "POST",
35
+ headers: {
36
+ Authorization: `Bearer ${token}`,
37
+ "Content-Type": "application/json",
38
+ },
39
+ body: JSON.stringify({
40
+ limit,
41
+ ...(cursor ? { cursor } : {}),
42
+ ...(dryRun ? { dryRun: true } : {}),
43
+ }),
44
+ });
45
+
46
+ if (!response.ok) {
47
+ const responseText = await response.text();
48
+ throw new Error(
49
+ `HTTP ${response.status}: ${responseText || response.statusText}`,
50
+ );
51
+ }
52
+
53
+ return response.json();
54
+ }
55
+
56
+ function logBatchResult(batchIndex, result) {
57
+ console.log(
58
+ ` batch ${batchIndex}: processed=${result.processed} wouldRebuild=${result.wouldRebuild} rebuilt=${result.rebuilt} wouldUpgradeFootnotes=${result.wouldUpgradeFootnotes ?? 0} upgradedFootnotes=${result.upgradedFootnotes ?? 0} skipped=${result.skipped} conflicted=${result.conflicted} failed=${result.failed}`,
59
+ );
60
+ for (const failure of result.failures ?? []) {
61
+ console.error(` ${failure.postId}: ${failure.error}`);
62
+ }
63
+ }
64
+
65
+ export async function run(argv) {
66
+ const { values } = parseArgs({
67
+ args: argv,
68
+ options: {
69
+ config: { type: "string" },
70
+ "dry-run": { type: "boolean", default: false },
71
+ env: { type: "string" },
72
+ help: { type: "boolean", short: "h" },
73
+ limit: { type: "string" },
74
+ once: { type: "boolean", default: false },
75
+ site: { type: "string" },
76
+ token: { type: "string" },
77
+ url: { type: "string" },
78
+ },
79
+ });
80
+
81
+ if (values.help) {
82
+ console.log("Usage: jant posts rebuild-html [--url <url>] [options]");
83
+ console.log("");
84
+ console.log(
85
+ "Upgrade recognized legacy footnotes and rebuild stored post body HTML.",
86
+ );
87
+ console.log(
88
+ "The operation is site-scoped, cursor-paginated, and idempotent.",
89
+ );
90
+ console.log("");
91
+ console.log("Options:");
92
+ console.log(" --url Target site or internal core URL");
93
+ console.log(
94
+ " --site Explicit managed site TypeID (host-based mode)",
95
+ );
96
+ console.log(
97
+ ` --limit Batch size (default: ${DEFAULT_BATCH_SIZE}, max: ${MAX_BATCH_SIZE})`,
98
+ );
99
+ console.log(" --dry-run Report changes without writing them");
100
+ console.log(" --once Run one batch and exit");
101
+ console.log(" --token Internal admin token");
102
+ console.log(" --config Wrangler config file");
103
+ console.log(" --env Wrangler environment name");
104
+ process.exit(0);
105
+ }
106
+
107
+ const siteUrl = resolveSiteUrl({
108
+ url: values.url,
109
+ config: values.config,
110
+ env: values.env,
111
+ });
112
+ if (!siteUrl) {
113
+ throw new Error(
114
+ "posts rebuild-html requires --url or SITE_ORIGIN in the environment or wrangler.toml.",
115
+ );
116
+ }
117
+
118
+ const token =
119
+ values.token?.trim() || process.env[INTERNAL_ADMIN_TOKEN_ENV_VAR]?.trim();
120
+ if (!token) {
121
+ throw new Error(
122
+ `posts rebuild-html requires --token or ${INTERNAL_ADMIN_TOKEN_ENV_VAR}.`,
123
+ );
124
+ }
125
+
126
+ const siteId = values.site?.trim();
127
+ const limit = parseBatchSize(values.limit);
128
+ const dryRun = values["dry-run"];
129
+ const rebuildUrl = buildRebuildUrl(siteUrl, siteId);
130
+ const scope = siteId ? `managed site ${siteId}` : siteUrl;
131
+
132
+ console.log(`${dryRun ? "Checking" : "Rebuilding"} post HTML for ${scope}`);
133
+ console.log(
134
+ ` batch size: ${limit}${values.once ? " (single batch)" : ""}${dryRun ? " (dry run)" : ""}`,
135
+ );
136
+
137
+ const totals = {
138
+ processed: 0,
139
+ wouldRebuild: 0,
140
+ rebuilt: 0,
141
+ wouldUpgradeFootnotes: 0,
142
+ upgradedFootnotes: 0,
143
+ skipped: 0,
144
+ conflicted: 0,
145
+ failed: 0,
146
+ };
147
+ let cursor;
148
+ let batchIndex = 0;
149
+ let targetVersion;
150
+
151
+ for (;;) {
152
+ batchIndex += 1;
153
+ const result = await requestBatch(rebuildUrl, token, {
154
+ limit,
155
+ cursor,
156
+ dryRun,
157
+ });
158
+ logBatchResult(batchIndex, result);
159
+
160
+ for (const key of Object.keys(totals)) {
161
+ totals[key] += result[key] ?? 0;
162
+ }
163
+ targetVersion = result.targetVersion;
164
+
165
+ if (values.once || result.done) break;
166
+ if (!result.nextCursor || result.nextCursor === cursor) {
167
+ throw new Error(
168
+ "Server reported more work without advancing the cursor; aborting.",
169
+ );
170
+ }
171
+ cursor = result.nextCursor;
172
+ }
173
+
174
+ console.log("");
175
+ console.log(
176
+ `Done. targetVersion=${targetVersion ?? "unknown"} processed=${totals.processed} wouldRebuild=${totals.wouldRebuild} rebuilt=${totals.rebuilt} wouldUpgradeFootnotes=${totals.wouldUpgradeFootnotes} upgradedFootnotes=${totals.upgradedFootnotes} skipped=${totals.skipped} conflicted=${totals.conflicted} failed=${totals.failed} batches=${batchIndex}`,
177
+ );
178
+
179
+ if (totals.failed > 0 || totals.conflicted > 0) {
180
+ throw new Error(
181
+ "Some rows were not rebuilt. Fix invalid bodies or rerun after concurrent edits settle.",
182
+ );
183
+ }
184
+ }
@@ -28,6 +28,7 @@ import {
28
28
  remapSnapshotObjectKey,
29
29
  rewriteLegacySnapshotSql,
30
30
  rewriteSnapshotSiteIdentifiers,
31
+ upgradeSnapshotSql,
31
32
  validateSnapshotTargetSite,
32
33
  } from "../../../lib/site-snapshot.js";
33
34
  import {
@@ -69,7 +70,7 @@ async function createNodeImportContext() {
69
70
  return nodeDatabase.query(sql);
70
71
  },
71
72
  async execute(sql) {
72
- await nodeDatabase.execute(sql);
73
+ await nodeDatabase.executeAtomically(sql);
73
74
  },
74
75
  async uploadObject(key, filePath, contentType) {
75
76
  if (!storage) {
@@ -334,6 +335,18 @@ export async function run(argv) {
334
335
  join(materialized.rootDir, "db.sql"),
335
336
  "utf-8",
336
337
  );
338
+ const siteScopedDbSql = snapshotSite
339
+ ? shouldRemapSite
340
+ ? rewriteSnapshotSiteIdentifiers(
341
+ rawDbSql,
342
+ snapshotSite.id,
343
+ targetSite.id,
344
+ )
345
+ : rawDbSql
346
+ : rewriteLegacySnapshotSql(rawDbSql, targetSite.id);
347
+ // Upgrade compatibility SQL before touching target rows or storage.
348
+ // A malformed v1 post_collection reference therefore fails safely.
349
+ const dbSql = upgradeSnapshotSql(siteScopedDbSql, meta.version);
337
350
 
338
351
  // Preflight: every storage_key/poster_key referenced by db.sql must have
339
352
  // a corresponding file in objects/, or we'll end up with broken media
@@ -389,15 +402,6 @@ export async function run(argv) {
389
402
  );
390
403
  }
391
404
 
392
- const dbSql = snapshotSite
393
- ? shouldRemapSite
394
- ? rewriteSnapshotSiteIdentifiers(
395
- rawDbSql,
396
- snapshotSite.id,
397
- targetSite.id,
398
- )
399
- : rawDbSql
400
- : rewriteLegacySnapshotSql(rawDbSql, targetSite.id);
401
405
  await context.execute(`${buildReplaceSql(targetSite.id)}\n${dbSql}`);
402
406
 
403
407
  const keysToDelete = [...currentKeys].filter(
@@ -87,6 +87,23 @@ function extractJson(raw) {
87
87
  return raw.slice(start);
88
88
  }
89
89
 
90
+ function getWranglerExecutionOptions(options) {
91
+ const executionOptions = { ...options };
92
+ for (const key of [
93
+ "configPath",
94
+ "database",
95
+ "env",
96
+ "persistTo",
97
+ "quiet",
98
+ "retryAttempts",
99
+ "retryDelayMs",
100
+ "trackedExecution",
101
+ ]) {
102
+ delete executionOptions[key];
103
+ }
104
+ return executionOptions;
105
+ }
106
+
90
107
  function runWrangler(args, options = {}) {
91
108
  const retryAttempts = Math.max(
92
109
  1,
@@ -99,7 +116,7 @@ function runWrangler(args, options = {}) {
99
116
 
100
117
  for (let attempt = 1; attempt <= retryAttempts; attempt += 1) {
101
118
  try {
102
- return runLocalWrangler(args, options);
119
+ return runLocalWrangler(args, getWranglerExecutionOptions(options));
103
120
  } catch (error) {
104
121
  const output = `${error.stdout ?? ""}${error.stderr ?? ""}`.trim();
105
122
  const wranglerError = parseWranglerError(output);
@@ -135,7 +152,7 @@ export function executeD1(sql, runtime, options = {}) {
135
152
  );
136
153
 
137
154
  if (options.quiet) {
138
- const output = runWrangler([...args, "--json"]);
155
+ const output = runWrangler([...args, "--json"], options);
139
156
  const parsed = JSON.parse(extractJson(output));
140
157
  const statements = Array.isArray(parsed) ? parsed : [parsed];
141
158
 
@@ -148,7 +165,7 @@ export function executeD1(sql, runtime, options = {}) {
148
165
  return statements;
149
166
  }
150
167
 
151
- runWrangler(args, { stdio: "inherit" });
168
+ runWrangler(args, { ...options, stdio: "inherit" });
152
169
  }
153
170
 
154
171
  export function queryD1(sql, runtime, options = {}) {
@@ -164,6 +181,7 @@ export function queryD1(sql, runtime, options = {}) {
164
181
  ],
165
182
  options,
166
183
  ),
184
+ options,
167
185
  );
168
186
  const parsed = JSON.parse(extractJson(output));
169
187
  const statement = Array.isArray(parsed) ? parsed[0] : parsed;
@@ -189,7 +207,7 @@ export function executeD1File(filePath, runtime, options = {}) {
189
207
  );
190
208
 
191
209
  if (options.quiet) {
192
- const output = runWrangler([...args, "--json"]);
210
+ const output = runWrangler([...args, "--json"], options);
193
211
  const parsed = JSON.parse(extractJson(output));
194
212
  const statements = Array.isArray(parsed) ? parsed : [parsed];
195
213
 
@@ -202,5 +220,5 @@ export function executeD1File(filePath, runtime, options = {}) {
202
220
  return statements;
203
221
  }
204
222
 
205
- runWrangler(args, { stdio: "inherit" });
223
+ runWrangler(args, { ...options, stdio: "inherit" });
206
224
  }
@@ -183,11 +183,21 @@ export function createD1SqlRunner(runtime, options) {
183
183
  executeD1(sql, runtime, { ...options, quiet: true });
184
184
  },
185
185
  executeTrackedFile(filePath, trackingSql) {
186
+ // A tracked schema/data migration is already atomic with its tracking
187
+ // insert. Do not let the lower-level Wrangler helper blindly replay a
188
+ // destructive batch after an ambiguous network response. The outer
189
+ // runner reconciles the tracking row instead.
190
+ const trackedOptions = {
191
+ ...options,
192
+ quiet: true,
193
+ retryAttempts: 1,
194
+ };
195
+
186
196
  if (trackedExecution === "segmented") {
187
197
  for (const statement of readSqlStatements(filePath)) {
188
- executeD1(statement, runtime, { ...options, quiet: true });
198
+ executeD1(statement, runtime, trackedOptions);
189
199
  }
190
- executeD1(trackingSql, runtime, { ...options, quiet: true });
200
+ executeD1(trackingSql, runtime, trackedOptions);
191
201
  return;
192
202
  }
193
203
 
@@ -200,7 +210,7 @@ export function createD1SqlRunner(runtime, options) {
200
210
  tempPath,
201
211
  `\n${readNormalizedSqlFile(filePath)}\n${trackingSql}\n`,
202
212
  );
203
- executeD1File(tempPath, runtime, { ...options, quiet: true });
213
+ executeD1File(tempPath, runtime, trackedOptions);
204
214
  } finally {
205
215
  rmSync(tempDir, { recursive: true, force: true });
206
216
  }
@@ -210,10 +220,7 @@ export function createD1SqlRunner(runtime, options) {
210
220
  executeD1(
211
221
  `\n${readNormalizedSqlFile(filePath)}\n${trackingSql}`,
212
222
  runtime,
213
- {
214
- ...options,
215
- quiet: true,
216
- },
223
+ trackedOptions,
217
224
  );
218
225
  },
219
226
  query(sql) {
@@ -261,6 +268,21 @@ export function applyTrackedSqlFiles(runner, options) {
261
268
  }
262
269
  console.log(`[${index + 1}/${pendingFiles.length}] ${file.name} ✅`);
263
270
  } catch (error) {
271
+ // The database may have committed the atomic batch even if the client
272
+ // lost its response. Re-read the tracking table before reporting a
273
+ // failure; a committed marker means replay would be both unnecessary and
274
+ // unsafe for CREATE/COPY/DROP migrations.
275
+ try {
276
+ if (new Set(listAppliedNames(runner, tableName)).has(file.name)) {
277
+ console.log(
278
+ `[${index + 1}/${pendingFiles.length}] ${file.name} ✅ (confirmed after interrupted response)`,
279
+ );
280
+ continue;
281
+ }
282
+ } catch {
283
+ // Preserve the original execution failure when reconciliation itself
284
+ // cannot reach the database.
285
+ }
264
286
  console.log(`[${index + 1}/${pendingFiles.length}] ${file.name} ❌`);
265
287
  throw new Error(`Failed to apply ${file.name}: ${error.message}`, {
266
288
  cause: error,
@@ -5,6 +5,15 @@ function hasExecute(db) {
5
5
  return typeof db === "object" && db !== null && "execute" in db;
6
6
  }
7
7
 
8
+ function hasTransaction(db) {
9
+ return (
10
+ typeof db === "object" &&
11
+ db !== null &&
12
+ "transaction" in db &&
13
+ typeof db.transaction === "function"
14
+ );
15
+ }
16
+
8
17
  export async function openNodeDatabase(env = process.env) {
9
18
  const { createNodeBindings } = await loadNodeRuntime();
10
19
  const { bindings, close } = await createNodeBindings({
@@ -44,5 +53,28 @@ export async function openNodeDatabase(env = process.env) {
44
53
 
45
54
  await database.db.run(sql);
46
55
  },
56
+ async executeAtomically(sql) {
57
+ if (bindings.NODE_SQLITE) {
58
+ bindings.NODE_SQLITE.transaction(() => {
59
+ bindings.NODE_SQLITE.exec(sql);
60
+ })();
61
+ return;
62
+ }
63
+
64
+ if (!hasTransaction(database.db)) {
65
+ throw new Error(
66
+ `Atomic SQL execution is unavailable for the ${database.dialect} Node database driver.`,
67
+ );
68
+ }
69
+
70
+ await database.db.transaction(async (transaction) => {
71
+ if (!hasExecute(transaction)) {
72
+ throw new Error(
73
+ `Atomic SQL execution is unavailable for the ${database.dialect} transaction driver.`,
74
+ );
75
+ }
76
+ await transaction.execute(sql);
77
+ });
78
+ },
47
79
  };
48
80
  }