@jant/core 0.6.16 → 0.7.1

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 (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -2,14 +2,21 @@
2
2
  * Static site client entry point (Hugo theme).
3
3
  *
4
4
  * Loaded on exported Hugo sites to enable reading-surface interactions —
5
- * media lightbox, feed video autoplay, audio waveform, and gallery scroll
6
- * hints. Does NOT include Datastar, auth, toast, or form plumbing; those
7
- * are runtime-only concerns for the authenticated app.
5
+ * media lightbox, feed video autoplay, audio waveform, gallery scroll hints,
6
+ * and the thread-context shell. Does NOT include Datastar, auth, toast, or
7
+ * form plumbing; those are runtime-only concerns for the authenticated app.
8
8
  */
9
9
 
10
10
  import "./client/audio-player.js";
11
11
  import "./client/feed-video-player.js";
12
12
  import "./client/media-scroll-hint.js";
13
13
  import "./client/site-header-nav.js";
14
+ // A thread preview's ancestor context is server-rendered collapsed, and its
15
+ // "Show more" is the only way back out of that. Off-site pages render the same
16
+ // shell — the Hugo export's own thread-preview partial had to leave the shell
17
+ // out entirely because this was missing — so the reading surface owes them the
18
+ // script that makes it work. It binds on DOMContentLoaded to
19
+ // `[data-thread-context-toggle]` and does nothing on a page carrying none.
20
+ import "./client/thread-context.js";
14
21
  import "./client/components/jant-media-lightbox.js";
15
22
  import "./styles/site-media.css";
package/src/client.ts CHANGED
@@ -11,8 +11,12 @@ import "./client/audio-player.js";
11
11
  import "./client/feed-video-player.js";
12
12
  import "./client/media-scroll-hint.js";
13
13
  import "./client/form-enter-submit.js";
14
+ import "./client/copy-field.js";
14
15
  import "./client/components/jant-media-lightbox.js";
15
16
  import "./client/components/jant-text-preview.js";
17
+ // First-run setup is a signed-out page, so the language picker it shares with
18
+ // the settings page has to ship in the public bundle.
19
+ import "./client/components/jant-locale-picker.js";
16
20
  import "./client/toast.js";
17
21
  import "./client/thread-context.js";
18
22
  import "./client/note-expand.js";
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Executes the real 0006 backfill SQL against snapshot-shaped nav rows.
3
+ *
4
+ * The backfill runs on user data through `jant migrate`, and the same file is
5
+ * executed on both SQLite and Postgres, so it has to be portable and safe to
6
+ * rerun. These tests cover the SQLite side; the SQL avoids dialect-specific
7
+ * syntax (no `UPDATE ... FROM`, only correlated subqueries).
8
+ */
9
+
10
+ import { readFileSync } from "fs";
11
+ import { resolve } from "path";
12
+ import type BetterSqlite3 from "better-sqlite3";
13
+ import { beforeEach, describe, expect, it } from "vitest";
14
+ import {
15
+ createTestDatabase,
16
+ DEFAULT_TEST_SITE_ID,
17
+ } from "../../__tests__/helpers/db.js";
18
+
19
+ const BACKFILL_SQL = readFileSync(
20
+ resolve(
21
+ import.meta.dirname,
22
+ "../backfills/0006_clear_mirrored_page_nav_labels.sql",
23
+ ),
24
+ "utf-8",
25
+ );
26
+
27
+ const POST_ID = "pst_backfill00000000000000001";
28
+ const COLLECTION_ID = "col_backfill00000000000000001";
29
+
30
+ describe("0006 backfill — clear nav labels that mirror their target", () => {
31
+ let sqlite: BetterSqlite3.Database;
32
+
33
+ function runBackfill() {
34
+ sqlite.exec(BACKFILL_SQL);
35
+ }
36
+
37
+ function labelOf(id: string): string {
38
+ const row = sqlite
39
+ .prepare(`SELECT label FROM nav_item WHERE id = ?`)
40
+ .get(id) as { label: string };
41
+ return row.label;
42
+ }
43
+
44
+ function insertNavItem(row: {
45
+ id: string;
46
+ type: "page" | "collection" | "link";
47
+ label: string;
48
+ position?: string;
49
+ postId?: string;
50
+ collectionId?: string;
51
+ }) {
52
+ sqlite
53
+ .prepare(
54
+ `INSERT INTO nav_item (
55
+ id, site_id, type, post_id, collection_id, label, url,
56
+ placement, position, created_at, updated_at
57
+ ) VALUES (?, ?, ?, ?, ?, ?, '/x', 'header', ?, 1, 1)`,
58
+ )
59
+ .run(
60
+ row.id,
61
+ DEFAULT_TEST_SITE_ID,
62
+ row.type,
63
+ row.postId ?? null,
64
+ row.collectionId ?? null,
65
+ row.label,
66
+ row.position ?? "a0",
67
+ );
68
+ }
69
+
70
+ beforeEach(() => {
71
+ sqlite = createTestDatabase().sqlite;
72
+ sqlite
73
+ .prepare(
74
+ `INSERT INTO post (
75
+ id, site_id, format, status, visibility, title, thread_id,
76
+ published_at, last_activity_at, created_at, updated_at
77
+ ) VALUES (?, ?, 'note', 'published', 'latest_hidden', 'About me', ?, 1, 1, 1, 1)`,
78
+ )
79
+ .run(POST_ID, DEFAULT_TEST_SITE_ID, POST_ID);
80
+ sqlite
81
+ .prepare(
82
+ `INSERT INTO collection (id, site_id, title, sort_order, created_at, updated_at)
83
+ VALUES (?, ?, 'Design Notes', 'newest', 1, 1)`,
84
+ )
85
+ .run(COLLECTION_ID, DEFAULT_TEST_SITE_ID);
86
+ });
87
+
88
+ it("clears a page label that only mirrors the post title", () => {
89
+ insertNavItem({
90
+ id: "nav_mirror0000000000000000001",
91
+ type: "page",
92
+ label: "About me",
93
+ postId: POST_ID,
94
+ });
95
+
96
+ runBackfill();
97
+
98
+ expect(labelOf("nav_mirror0000000000000000001")).toBe("");
99
+ });
100
+
101
+ it("clears a collection label that only mirrors the collection title", () => {
102
+ insertNavItem({
103
+ id: "nav_mirror0000000000000000002",
104
+ type: "collection",
105
+ label: "Design Notes",
106
+ collectionId: COLLECTION_ID,
107
+ });
108
+
109
+ runBackfill();
110
+
111
+ expect(labelOf("nav_mirror0000000000000000002")).toBe("");
112
+ });
113
+
114
+ it("keeps a label the author typed", () => {
115
+ insertNavItem({
116
+ id: "nav_custom0000000000000000001",
117
+ type: "page",
118
+ label: "Colophon",
119
+ postId: POST_ID,
120
+ });
121
+ insertNavItem({
122
+ id: "nav_custom0000000000000000002",
123
+ type: "link",
124
+ label: "About me",
125
+ position: "a1",
126
+ });
127
+
128
+ runBackfill();
129
+
130
+ expect(labelOf("nav_custom0000000000000000001")).toBe("Colophon");
131
+ // A free-form link has no target to follow, so its label is never cleared
132
+ // even when it happens to read like one.
133
+ expect(labelOf("nav_custom0000000000000000002")).toBe("About me");
134
+ });
135
+
136
+ it("is safe to rerun", () => {
137
+ insertNavItem({
138
+ id: "nav_mirror0000000000000000003",
139
+ type: "page",
140
+ label: "About me",
141
+ postId: POST_ID,
142
+ });
143
+
144
+ runBackfill();
145
+ runBackfill();
146
+
147
+ expect(labelOf("nav_mirror0000000000000000003")).toBe("");
148
+ });
149
+ });
@@ -1,3 +1,4 @@
1
+ import { existsSync, readFileSync } from "node:fs";
1
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
3
 
3
4
  const { runLocalWranglerMock } = vi.hoisted(() => ({
@@ -8,7 +9,7 @@ vi.mock("../../../bin/lib/wrangler-cli.js", () => ({
8
9
  runLocalWrangler: runLocalWranglerMock,
9
10
  }));
10
11
 
11
- const { executeD1, parseWranglerError, queryD1 } =
12
+ const { exceedsInlineCommandLimit, executeD1, parseWranglerError, queryD1 } =
12
13
  await import("../../../bin/lib/d1-query.js");
13
14
 
14
15
  function createWranglerError(stderr) {
@@ -124,3 +125,57 @@ describe("d1-query Wrangler error parsing", () => {
124
125
  );
125
126
  });
126
127
  });
128
+
129
+ /**
130
+ * Oversized statement batches.
131
+ *
132
+ * A batch travels to Wrangler as one `--command=` argument, and Linux caps a
133
+ * single argv entry at 128 KiB (`MAX_ARG_STRLEN`). macOS has no such cap, so
134
+ * the first snapshot whose `db.sql` grew past it imported fine on a Mac and
135
+ * failed CI with `E2BIG`. Anything that large has to go through `--file`.
136
+ */
137
+ describe("d1-query oversized batches", () => {
138
+ beforeEach(() => {
139
+ runLocalWranglerMock.mockReset();
140
+ });
141
+
142
+ it("sends a small batch inline", () => {
143
+ runLocalWranglerMock.mockReturnValue(JSON.stringify([{ success: true }]));
144
+
145
+ executeD1("SELECT 1;", "d1-local", { quiet: true, retryAttempts: 1 });
146
+
147
+ expect(runLocalWranglerMock).toHaveBeenCalledWith(
148
+ expect.arrayContaining(["--command=SELECT 1;"]),
149
+ expect.anything(),
150
+ );
151
+ });
152
+
153
+ it("writes a batch too large for one argument to a file", () => {
154
+ const sql = `${"INSERT INTO t VALUES ('x');".repeat(8000)}\n`;
155
+ expect(exceedsInlineCommandLimit(sql)).toBe(true);
156
+
157
+ let seenPath: string | undefined;
158
+ let seenContents: string | undefined;
159
+ runLocalWranglerMock.mockImplementation((args: string[]) => {
160
+ const index = args.indexOf("--file");
161
+ seenPath = args[index + 1];
162
+ seenContents = readFileSync(seenPath as string, "utf8");
163
+ return JSON.stringify([{ success: true }]);
164
+ });
165
+
166
+ executeD1(sql, "d1-local", { quiet: true, retryAttempts: 1 });
167
+
168
+ const args = runLocalWranglerMock.mock.calls[0]?.[0] as string[];
169
+ expect(args).toContain("--file");
170
+ expect(args.some((arg) => arg.startsWith("--command="))).toBe(false);
171
+ expect(seenContents).toBe(sql);
172
+ // The temp file is Wrangler's to read and ours to clean up.
173
+ expect(existsSync(seenPath as string)).toBe(false);
174
+ });
175
+
176
+ it("keeps the threshold under the Linux single-argument cap", () => {
177
+ const MAX_ARG_STRLEN = 128 * 1024;
178
+ const justUnder = "a".repeat(MAX_ARG_STRLEN - 1);
179
+ expect(exceedsInlineCommandLimit(justUnder)).toBe(true);
180
+ });
181
+ });
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ /**
6
+ * The schema files must not keep private copies of the value lists that
7
+ * `types/constants.ts` already owns.
8
+ *
9
+ * This is not a style rule. A CHECK constraint is generated from whatever list
10
+ * the schema file can see, so a private copy that stops matching the shared one
11
+ * produces a constraint that rejects rows the application considers valid — and
12
+ * the schema files are the one place where nothing else would catch it, because
13
+ * `drizzle-kit generate` happily emits the wrong constraint and the types still
14
+ * line up on both sides.
15
+ */
16
+ const SCHEMA_FILES = ["../schema.ts", "../pg/schema.ts"];
17
+
18
+ /** `const NAME = [...] as const`, the shape a value list is declared in. */
19
+ const LOCAL_VALUE_LIST =
20
+ /const\s+([A-Z][A-Z0-9_]*)\s*=\s*\[[^\]]*\]\s*as const/g;
21
+
22
+ describe("schema value lists", () => {
23
+ for (const file of SCHEMA_FILES) {
24
+ it(`${file} declares no value list of its own`, () => {
25
+ const source = readFileSync(resolve(__dirname, file), "utf8");
26
+ const declared = [...source.matchAll(LOCAL_VALUE_LIST)].map(
27
+ (match) => match[1],
28
+ );
29
+
30
+ expect(declared).toEqual([]);
31
+ });
32
+ }
33
+ });
@@ -0,0 +1,402 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import type Database from "better-sqlite3";
3
+ import {
4
+ createTestDatabase,
5
+ DEFAULT_TEST_SITE_ID,
6
+ } from "../../__tests__/helpers/db.js";
7
+
8
+ /**
9
+ * The three tables a smart collection can be placed in, checked by inserting
10
+ * real rows.
11
+ *
12
+ * Reading a CHECK constraint and believing it is not the same as running it.
13
+ * The last time these constraints were written by hand, a test shaped exactly
14
+ * like this one found three bugs in a single pass: a branch that forgot to
15
+ * require its own foreign key, a branch that allowed two of them at once, and a
16
+ * unique index whose partial predicate never matched.
17
+ *
18
+ * Each case states the row it inserts and what the schema promises about it, so
19
+ * a failure names the rule rather than the constraint.
20
+ */
21
+
22
+ const SITE = DEFAULT_TEST_SITE_ID;
23
+ const NOW = 1_700_000_000;
24
+
25
+ function ids(prefix: string, n: number): string[] {
26
+ return Array.from(
27
+ { length: n },
28
+ (_, i) => `${prefix}_0000000000000000000000000${i}`,
29
+ );
30
+ }
31
+
32
+ describe("smart collection placement constraints", () => {
33
+ let sqlite: Database.Database;
34
+ let smartA: string;
35
+ let smartB: string;
36
+ let collectionA: string;
37
+ let postA: string;
38
+
39
+ beforeEach(() => {
40
+ ({ sqlite } = createTestDatabase());
41
+ [smartA, smartB] = ids("smc", 2) as [string, string];
42
+ [collectionA] = ids("col", 1) as [string];
43
+ [postA] = ids("pst", 1) as [string];
44
+
45
+ sqlite
46
+ .prepare(
47
+ `INSERT INTO collection (id, site_id, title, sort_order, created_at, updated_at)
48
+ VALUES (?, ?, 'Books', 'newest', ?, ?)`,
49
+ )
50
+ .run(collectionA, SITE, NOW, NOW);
51
+
52
+ for (const id of [smartA, smartB]) {
53
+ sqlite
54
+ .prepare(
55
+ `INSERT INTO smart_collection (id, site_id, title, sort, created_at, updated_at)
56
+ VALUES (?, ?, 'Quotes', 'newest', ?, ?)`,
57
+ )
58
+ .run(id, SITE, NOW, NOW);
59
+ }
60
+
61
+ sqlite
62
+ .prepare(
63
+ `INSERT INTO post (
64
+ id, site_id, thread_id, format, status, visibility,
65
+ body_html, body_text, created_at, updated_at
66
+ ) VALUES (?, ?, ?, 'note', 'published', 'public', '', '', ?, ?)`,
67
+ )
68
+ .run(postA, SITE, postA, NOW, NOW);
69
+ });
70
+
71
+ function insertPath(values: Record<string, unknown>) {
72
+ const row = {
73
+ id: `pth_${Math.random().toString(36).slice(2, 12)}`,
74
+ site_id: SITE,
75
+ path: `p-${Math.random().toString(36).slice(2, 8)}`,
76
+ kind: "slug",
77
+ post_id: null,
78
+ collection_id: null,
79
+ smart_collection_id: null,
80
+ redirect_to_path: null,
81
+ redirect_type: null,
82
+ archive_query: null,
83
+ created_at: NOW,
84
+ updated_at: NOW,
85
+ ...values,
86
+ };
87
+ const keys = Object.keys(row);
88
+ sqlite
89
+ .prepare(
90
+ `INSERT INTO path_registry (${keys.join(", ")})
91
+ VALUES (${keys.map(() => "?").join(", ")})`,
92
+ )
93
+ .run(...keys.map((key) => row[key]));
94
+ }
95
+
96
+ function insertDirectoryItem(values: Record<string, unknown>) {
97
+ const row = {
98
+ id: `cdi_${Math.random().toString(36).slice(2, 12)}`,
99
+ site_id: SITE,
100
+ type: "smart_collection",
101
+ collection_id: null,
102
+ smart_collection_id: null,
103
+ label: null,
104
+ url: null,
105
+ description: null,
106
+ position: `a${Math.random().toString(36).slice(2, 8)}`,
107
+ created_at: NOW,
108
+ updated_at: NOW,
109
+ ...values,
110
+ };
111
+ const keys = Object.keys(row);
112
+ sqlite
113
+ .prepare(
114
+ `INSERT INTO collection_directory_item (${keys.join(", ")})
115
+ VALUES (${keys.map(() => "?").join(", ")})`,
116
+ )
117
+ .run(...keys.map((key) => row[key]));
118
+ }
119
+
120
+ function insertNavItem(values: Record<string, unknown>) {
121
+ const row = {
122
+ id: `nav_${Math.random().toString(36).slice(2, 12)}`,
123
+ site_id: SITE,
124
+ type: "smart_collection",
125
+ system_key: null,
126
+ collection_id: null,
127
+ smart_collection_id: null,
128
+ post_id: null,
129
+ label: "",
130
+ url: "/quotes",
131
+ placement: "header",
132
+ position: `a${Math.random().toString(36).slice(2, 8)}`,
133
+ created_at: NOW,
134
+ updated_at: NOW,
135
+ ...values,
136
+ };
137
+ const keys = Object.keys(row);
138
+ sqlite
139
+ .prepare(
140
+ `INSERT INTO nav_item (${keys.join(", ")})
141
+ VALUES (${keys.map(() => "?").join(", ")})`,
142
+ )
143
+ .run(...keys.map((key) => row[key]));
144
+ }
145
+
146
+ describe("path_registry", () => {
147
+ it("accepts a slug pointing at a smart collection", () => {
148
+ expect(() =>
149
+ insertPath({ smart_collection_id: smartA, path: "quotes" }),
150
+ ).not.toThrow();
151
+ });
152
+
153
+ it("accepts an alias pointing at a smart collection", () => {
154
+ expect(() =>
155
+ insertPath({
156
+ kind: "alias",
157
+ smart_collection_id: smartA,
158
+ path: "old-quotes",
159
+ }),
160
+ ).not.toThrow();
161
+ });
162
+
163
+ it("refuses a slug that points at two things at once", () => {
164
+ expect(() =>
165
+ insertPath({ smart_collection_id: smartA, collection_id: collectionA }),
166
+ ).toThrow();
167
+ expect(() =>
168
+ insertPath({ smart_collection_id: smartA, post_id: postA }),
169
+ ).toThrow();
170
+ });
171
+
172
+ it("refuses a redirect or archive row that names a smart collection", () => {
173
+ expect(() =>
174
+ insertPath({
175
+ kind: "redirect",
176
+ smart_collection_id: smartA,
177
+ redirect_to_path: "quotes",
178
+ redirect_type: 301,
179
+ }),
180
+ ).toThrow();
181
+ expect(() =>
182
+ insertPath({
183
+ kind: "archive",
184
+ smart_collection_id: smartA,
185
+ archive_query: "format=quote",
186
+ }),
187
+ ).toThrow();
188
+ });
189
+
190
+ it("allows only one slug per smart collection", () => {
191
+ insertPath({ smart_collection_id: smartA, path: "quotes" });
192
+ expect(() =>
193
+ insertPath({ smart_collection_id: smartA, path: "quotes-2" }),
194
+ ).toThrow();
195
+
196
+ // An alias is not a second slug, so it is still allowed.
197
+ expect(() =>
198
+ insertPath({
199
+ kind: "alias",
200
+ smart_collection_id: smartA,
201
+ path: "old-quotes",
202
+ }),
203
+ ).not.toThrow();
204
+ });
205
+
206
+ it("keeps smart collections in the same address space as everything else", () => {
207
+ insertPath({ smart_collection_id: smartA, path: "quotes" });
208
+ expect(() =>
209
+ insertPath({ collection_id: collectionA, path: "quotes" }),
210
+ ).toThrow();
211
+ expect(() => insertPath({ post_id: postA, path: "quotes" })).toThrow();
212
+ });
213
+ });
214
+
215
+ describe("collection_directory_item", () => {
216
+ it("accepts a smart collection row", () => {
217
+ expect(() =>
218
+ insertDirectoryItem({ smart_collection_id: smartA }),
219
+ ).not.toThrow();
220
+ });
221
+
222
+ it("refuses a smart collection row with no target", () => {
223
+ expect(() => insertDirectoryItem({})).toThrow();
224
+ });
225
+
226
+ it("refuses a smart collection row carrying a label or url", () => {
227
+ expect(() =>
228
+ insertDirectoryItem({ smart_collection_id: smartA, label: "Quotes" }),
229
+ ).toThrow();
230
+ expect(() =>
231
+ insertDirectoryItem({ smart_collection_id: smartA, url: "/quotes" }),
232
+ ).toThrow();
233
+ });
234
+
235
+ it("refuses every other row type carrying a smart collection", () => {
236
+ expect(() =>
237
+ insertDirectoryItem({
238
+ type: "collection",
239
+ collection_id: collectionA,
240
+ smart_collection_id: smartA,
241
+ }),
242
+ ).toThrow();
243
+ expect(() =>
244
+ insertDirectoryItem({ type: "divider", smart_collection_id: smartA }),
245
+ ).toThrow();
246
+ expect(() =>
247
+ insertDirectoryItem({
248
+ type: "link",
249
+ label: "Elsewhere",
250
+ url: "https://example.com",
251
+ smart_collection_id: smartA,
252
+ }),
253
+ ).toThrow();
254
+ });
255
+
256
+ it("places a smart collection at most once", () => {
257
+ insertDirectoryItem({ smart_collection_id: smartA });
258
+ expect(() =>
259
+ insertDirectoryItem({ smart_collection_id: smartA }),
260
+ ).toThrow();
261
+ expect(() =>
262
+ insertDirectoryItem({ smart_collection_id: smartB }),
263
+ ).not.toThrow();
264
+ });
265
+ });
266
+
267
+ describe("nav_item", () => {
268
+ it("accepts a smart collection nav item", () => {
269
+ expect(() =>
270
+ insertNavItem({ smart_collection_id: smartA }),
271
+ ).not.toThrow();
272
+ });
273
+
274
+ it("refuses a smart collection nav item with no target", () => {
275
+ expect(() => insertNavItem({})).toThrow();
276
+ });
277
+
278
+ it("refuses every other nav type carrying a smart collection", () => {
279
+ expect(() =>
280
+ insertNavItem({
281
+ type: "link",
282
+ smart_collection_id: smartA,
283
+ label: "Elsewhere",
284
+ }),
285
+ ).toThrow();
286
+ expect(() =>
287
+ insertNavItem({
288
+ type: "system",
289
+ system_key: "latest",
290
+ smart_collection_id: smartA,
291
+ }),
292
+ ).toThrow();
293
+ expect(() =>
294
+ insertNavItem({
295
+ type: "collection",
296
+ collection_id: collectionA,
297
+ smart_collection_id: smartA,
298
+ }),
299
+ ).toThrow();
300
+ expect(() =>
301
+ insertNavItem({
302
+ type: "page",
303
+ post_id: postA,
304
+ smart_collection_id: smartA,
305
+ }),
306
+ ).toThrow();
307
+ });
308
+
309
+ it("adds a smart collection to navigation at most once", () => {
310
+ insertNavItem({ smart_collection_id: smartA });
311
+ expect(() => insertNavItem({ smart_collection_id: smartA })).toThrow();
312
+ expect(() =>
313
+ insertNavItem({ smart_collection_id: smartB }),
314
+ ).not.toThrow();
315
+ });
316
+ });
317
+
318
+ describe("smart_collection", () => {
319
+ it("refuses a private visibility condition", () => {
320
+ // The one value a published page can never name. Enforced in the column,
321
+ // not only in Zod, so no import path or backfill can slip one in.
322
+ expect(() =>
323
+ sqlite
324
+ .prepare(
325
+ `INSERT INTO smart_collection (id, site_id, title, visibility, sort, created_at, updated_at)
326
+ VALUES (?, ?, 'Private', 'private', 'newest', ?, ?)`,
327
+ )
328
+ .run(`smc_private0000000000000000000`, SITE, NOW, NOW),
329
+ ).toThrow();
330
+ });
331
+
332
+ it("refuses a sort or layout it cannot render", () => {
333
+ expect(() =>
334
+ sqlite
335
+ .prepare(
336
+ `INSERT INTO smart_collection (id, site_id, title, sort, created_at, updated_at)
337
+ VALUES (?, ?, 'Bad sort', 'rating_asc', ?, ?)`,
338
+ )
339
+ .run(`smc_badsort00000000000000000000`, SITE, NOW, NOW),
340
+ ).toThrow();
341
+ expect(() =>
342
+ sqlite
343
+ .prepare(
344
+ `INSERT INTO smart_collection (id, site_id, title, sort, layout, created_at, updated_at)
345
+ VALUES (?, ?, 'Bad layout', 'newest', 'carousel', ?, ?)`,
346
+ )
347
+ .run(`smc_badlayout00000000000000000`, SITE, NOW, NOW),
348
+ ).toThrow();
349
+ });
350
+
351
+ it("leaves a deleted collection's id in place rather than widening", () => {
352
+ sqlite
353
+ .prepare(`UPDATE smart_collection SET collection_id = ? WHERE id = ?`)
354
+ .run(collectionA, smartA);
355
+
356
+ // No foreign key on this column, alone in the schema. RESTRICT would fail
357
+ // anonymously and make every bulk delete an ordering rule; SET NULL would
358
+ // turn a curated page into the whole archive. The refusal an author sees
359
+ // is `smartCollections.assertCollectionUnused()`, and what is left if
360
+ // something reaches around it is a condition naming a collection that no
361
+ // longer exists — which matches nothing.
362
+ expect(() =>
363
+ sqlite.prepare(`DELETE FROM collection WHERE id = ?`).run(collectionA),
364
+ ).not.toThrow();
365
+
366
+ const row = sqlite
367
+ .prepare(`SELECT collection_id FROM smart_collection WHERE id = ?`)
368
+ .get(smartA) as { collection_id: string | null };
369
+ expect(row.collection_id).toBe(collectionA);
370
+ });
371
+
372
+ it("refuses a year no timestamp could carry", () => {
373
+ // `Date.UTC` returns NaN past year 275760, and a NaN bound is a
374
+ // comparison every row fails with nothing to report.
375
+ for (const year of [1970, 10000]) {
376
+ expect(() =>
377
+ sqlite
378
+ .prepare(
379
+ `INSERT INTO smart_collection (id, site_id, title, year, sort, created_at, updated_at)
380
+ VALUES (?, ?, 'Bad year', ?, 'newest', ?, ?)`,
381
+ )
382
+ .run(
383
+ `smc_year${year}000000000000000000`.slice(0, 30),
384
+ SITE,
385
+ year,
386
+ NOW,
387
+ NOW,
388
+ ),
389
+ ).toThrow();
390
+ }
391
+
392
+ expect(() =>
393
+ sqlite
394
+ .prepare(
395
+ `INSERT INTO smart_collection (id, site_id, title, year, sort, created_at, updated_at)
396
+ VALUES (?, ?, 'Edge years', 9999, 'newest', ?, ?)`,
397
+ )
398
+ .run(`smc_year9999000000000000000000`.slice(0, 30), SITE, NOW, NOW),
399
+ ).not.toThrow();
400
+ });
401
+ });
402
+ });