@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
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Round-trip budget for the post page.
3
+ *
4
+ * On Workers a public page's cost is almost entirely the number of sequential
5
+ * D1 reads it makes — the same render against local SQLite is about a
6
+ * millisecond. Serving `/{slug}` used to read `path_registry` twice for one
7
+ * address and the `post` table four times for one post, and nothing failed
8
+ * when it did. These tests are that missing failure.
9
+ */
10
+
11
+ import { describe, expect, it, vi } from "vitest";
12
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
13
+ import { withConfig } from "../../../middleware/config.js";
14
+ import { i18nMiddleware } from "../../../i18n/index.js";
15
+ import { normalizePath, toPublicHref } from "../../../lib/url.js";
16
+ import { pageRoutes } from "../page.js";
17
+
18
+ /**
19
+ * Mount the catch-all behind the middleware `createApp()` puts in front of it.
20
+ *
21
+ * The stored-redirect middleware is reproduced rather than imported because it
22
+ * is defined inline in `app.tsx`; what is under test is that it hands its
23
+ * lookup on and the route takes it, so the stash is the part that must match.
24
+ */
25
+ function createPostPageApp() {
26
+ const testApp = createTestApp();
27
+ const { app } = testApp;
28
+
29
+ app.use("*", async (c, next) => {
30
+ c.set("publicPath", c.req.path);
31
+ c.set("publicRequestUrl", c.req.url);
32
+ await next();
33
+ });
34
+
35
+ app.use("*", async (c, next) => {
36
+ const storedPath = normalizePath(new URL(c.req.url).pathname);
37
+ const record = await c.var.services.paths.resolve(storedPath);
38
+ c.set("pathLookup", { path: storedPath, record });
39
+ if (record?.kind === "redirect" && record.redirectToPath) {
40
+ return c.redirect(
41
+ toPublicHref(`/${record.redirectToPath}`),
42
+ record.redirectType ?? 301,
43
+ );
44
+ }
45
+ await next();
46
+ });
47
+ app.use("*", withConfig());
48
+ app.use("*", i18nMiddleware());
49
+ app.route("/", pageRoutes);
50
+
51
+ return testApp;
52
+ }
53
+
54
+ /** Every SQL statement prepared while `run` executes. */
55
+ async function captureStatements<T>(
56
+ sqlite: ReturnType<typeof createTestApp>["sqlite"],
57
+ run: () => Promise<T>,
58
+ ): Promise<[T, string[]]> {
59
+ const original = sqlite.prepare.bind(sqlite);
60
+ const statements: string[] = [];
61
+ const spy = vi.spyOn(sqlite, "prepare").mockImplementation(((
62
+ source: string,
63
+ ) => {
64
+ statements.push(source.replace(/\s+/g, " "));
65
+ return original(source);
66
+ }) as typeof sqlite.prepare);
67
+ try {
68
+ return [await run(), statements];
69
+ } finally {
70
+ spy.mockRestore();
71
+ }
72
+ }
73
+
74
+ /** Reads that resolve one address, i.e. filter `path_registry` by `path`. */
75
+ function countAddressLookups(statements: string[]): number {
76
+ return statements.filter(
77
+ (sql) =>
78
+ /from "path_registry"/i.test(sql) &&
79
+ /"path_registry"\."path" = \?/i.test(sql),
80
+ ).length;
81
+ }
82
+
83
+ /** Reads that scan the `post` table. */
84
+ function countPostReads(statements: string[]): number {
85
+ return statements.filter((sql) => /from "post"/i.test(sql)).length;
86
+ }
87
+
88
+ describe("post page round trips", () => {
89
+ it("resolves the requested address once, and keeps its post reads to three", async () => {
90
+ const { app, services, sqlite } = createPostPageApp();
91
+ const post = await services.posts.create({
92
+ format: "note",
93
+ title: "Budget",
94
+ bodyMarkdown: "Body",
95
+ status: "published",
96
+ });
97
+
98
+ const [res, statements] = await captureStatements(sqlite, () =>
99
+ app.request(`/${post.slug}`),
100
+ );
101
+ expect(res.status).toBe(200);
102
+
103
+ // The middleware's lookup is the route's lookup.
104
+ expect(countAddressLookups(statements)).toBe(1);
105
+
106
+ // Three reads, and each is for something different: the post with its
107
+ // canonical alias, its thread, and the thread ids that collection
108
+ // membership is keyed by. A root post's visibility rides along in its own
109
+ // row, so nothing goes back for that.
110
+ expect(countPostReads(statements)).toBe(3);
111
+ });
112
+
113
+ it("redirects a slug to its custom URL without a separate alias lookup", async () => {
114
+ const { app, services, sqlite } = createPostPageApp();
115
+ const post = await services.posts.create({
116
+ format: "note",
117
+ title: "Renamed",
118
+ bodyMarkdown: "Body",
119
+ status: "published",
120
+ });
121
+ await services.customUrls.create({
122
+ path: "renamed-url",
123
+ targetType: "post",
124
+ targetId: post.id,
125
+ });
126
+
127
+ const [res, statements] = await captureStatements(sqlite, () =>
128
+ app.request(`/${post.slug}`),
129
+ );
130
+ expect(res.status).toBe(301);
131
+ expect(res.headers.get("location")).toBe("/renamed-url");
132
+
133
+ // The address lookup, and nothing more: the alias came back with the post.
134
+ expect(countAddressLookups(statements)).toBe(1);
135
+ expect(countPostReads(statements)).toBe(1);
136
+ });
137
+
138
+ it("takes the newest alias as the canonical address", async () => {
139
+ const { app, services } = createPostPageApp();
140
+ const post = await services.posts.create({
141
+ format: "note",
142
+ title: "Renamed twice",
143
+ bodyMarkdown: "Body",
144
+ status: "published",
145
+ });
146
+ await services.customUrls.create({
147
+ path: "first-name",
148
+ targetType: "post",
149
+ targetId: post.id,
150
+ });
151
+ await services.customUrls.create({
152
+ path: "second-name",
153
+ targetType: "post",
154
+ targetId: post.id,
155
+ });
156
+
157
+ const res = await app.request(`/${post.slug}`);
158
+ expect(res.status).toBe(301);
159
+ expect(res.headers.get("location")).toBe("/second-name");
160
+ });
161
+ });
162
+
163
+ describe("post list round trips", () => {
164
+ it("reads thread-root visibility from the rows it already loaded", async () => {
165
+ const { services, sqlite } = createTestApp();
166
+ for (let i = 0; i < 3; i++) {
167
+ await services.posts.create({
168
+ format: "note",
169
+ title: `Post ${i}`,
170
+ bodyMarkdown: "Body",
171
+ status: "published",
172
+ });
173
+ }
174
+
175
+ const [listed, statements] = await captureStatements(sqlite, () =>
176
+ services.posts.list({ status: "published", excludeReplies: true }),
177
+ );
178
+
179
+ expect(listed).toHaveLength(3);
180
+ // A list of thread roots carries its own visibility, so the only `post`
181
+ // read is the list itself.
182
+ expect(countPostReads(statements)).toBe(1);
183
+ });
184
+
185
+ it("still looks up the root for a reply loaded on its own", async () => {
186
+ const { services, sqlite } = createTestApp();
187
+ const root = await services.posts.create({
188
+ format: "note",
189
+ title: "Root",
190
+ bodyMarkdown: "Body",
191
+ status: "published",
192
+ visibility: "private",
193
+ });
194
+ const reply = await services.posts.create({
195
+ format: "note",
196
+ bodyMarkdown: "Reply",
197
+ replyToId: root.id,
198
+ status: "published",
199
+ });
200
+
201
+ const [loaded, statements] = await captureStatements(sqlite, () =>
202
+ services.posts.getById(reply.id),
203
+ );
204
+
205
+ // A reply's own visibility column is null — it inherits the root's — and
206
+ // the root is not among the rows loaded here, so the second read is what
207
+ // makes the reply private too.
208
+ expect(countPostReads(statements)).toBe(2);
209
+ expect(loaded?.visibility).toBe("private");
210
+ });
211
+ });
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import { partialPageRoutes } from "../partials.js";
4
+
5
+ function createPartialTestApp(options: { authenticated?: boolean } = {}) {
6
+ const testApp = createTestApp(options);
7
+ testApp.app.route("/", partialPageRoutes);
8
+ return testApp;
9
+ }
10
+
11
+ describe("/_/post-preview/:postId", () => {
12
+ it("renders the post the way its own page does", async () => {
13
+ // The composer shows this while writing a translation, and the original's
14
+ // structure is part of what gets translated — so it has to arrive as the
15
+ // post, not as a body-only excerpt.
16
+ const { app, services } = createPartialTestApp({ authenticated: true });
17
+ const post = await services.posts.create({
18
+ format: "note",
19
+ title: "Yirgacheffe",
20
+ bodyMarkdown: "## Brewing\n\nNinety-two degrees was too hot.",
21
+ status: "published",
22
+ });
23
+
24
+ const res = await app.request(`/_/post-preview/${post.id}`);
25
+
26
+ expect(res.status).toBe(200);
27
+ const html = await res.text();
28
+ expect(html).toContain("Yirgacheffe");
29
+ expect(html).toContain("Brewing");
30
+ expect(html).toContain("Ninety-two degrees was too hot.");
31
+ // Detail styling, not the feed card's summary.
32
+ expect(html).toContain("post-detail-title");
33
+ });
34
+
35
+ it("carries a quote's attribution, which a body-only preview would drop", async () => {
36
+ const { app, services } = createPartialTestApp({ authenticated: true });
37
+ // A Quote stores its attribution in `title` / `url`; the API surfaces the
38
+ // same two as `sourceName` / `sourceUrl`.
39
+ const post = await services.posts.create({
40
+ format: "quote",
41
+ quoteText: "Focus is about saying no.",
42
+ title: "Some Author",
43
+ url: "https://example.com/focus",
44
+ status: "published",
45
+ });
46
+
47
+ const html = await (await app.request(`/_/post-preview/${post.id}`)).text();
48
+
49
+ expect(html).toContain("Focus is about saying no.");
50
+ expect(html).toContain("Some Author");
51
+ expect(html).toContain("https://example.com/focus");
52
+ });
53
+
54
+ it("offers no control that would act on the post being previewed", async () => {
55
+ // Every action in the composer belongs to the post being written. A menu
56
+ // or reply trigger here would aim at the wrong one.
57
+ const { app, services } = createPartialTestApp({ authenticated: true });
58
+ const post = await services.posts.create({
59
+ format: "note",
60
+ title: "Yirgacheffe",
61
+ bodyMarkdown: "Body",
62
+ status: "published",
63
+ });
64
+
65
+ const html = await (await app.request(`/_/post-preview/${post.id}`)).text();
66
+
67
+ expect(html).not.toContain("data-post-menu-trigger");
68
+ expect(html).not.toContain("data-reply-trigger");
69
+ });
70
+
71
+ it("prints a titled post's date once, not in both header and footer", async () => {
72
+ // The card drops the footer's copy for a titled detail post, but only when
73
+ // `hideTimestamp` is left undefined — stating it either way brings it back.
74
+ const { app, services } = createPartialTestApp({ authenticated: true });
75
+ const post = await services.posts.create({
76
+ format: "note",
77
+ title: "Yirgacheffe",
78
+ bodyMarkdown: "Body",
79
+ status: "published",
80
+ });
81
+
82
+ const html = await (await app.request(`/_/post-preview/${post.id}`)).text();
83
+
84
+ expect(html.match(/<time/g) ?? []).toHaveLength(1);
85
+ });
86
+
87
+ it("404s on a post that is not there", async () => {
88
+ const { app } = createPartialTestApp({ authenticated: true });
89
+
90
+ const res = await app.request(
91
+ "/_/post-preview/pst_01000000000000000000000000",
92
+ );
93
+
94
+ expect(res.status).toBe(404);
95
+ });
96
+ });
@@ -0,0 +1,298 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import { pageRoutes } from "../page.js";
4
+
5
+ /**
6
+ * The smart collection page, end to end through the path registry.
7
+ *
8
+ * The property these guard is the one the design turns on: a smart collection
9
+ * is always public, and the posts inside it are exactly as visible as they are
10
+ * anywhere else. Those are different sentences, and confusing them is how a
11
+ * page ends up either 404ing for readers or leaking a draft.
12
+ */
13
+
14
+ function setup(authenticated = false) {
15
+ const { app, services } = createTestApp({ authenticated });
16
+ app.route("/", pageRoutes);
17
+ return { app, services };
18
+ }
19
+
20
+ async function seedQuotes(services: {
21
+ posts: {
22
+ create: (input: Record<string, unknown>) => Promise<{ id: string }>;
23
+ };
24
+ }) {
25
+ await services.posts.create({
26
+ format: "quote",
27
+ quoteText: "A public quote",
28
+ bodyMarkdown: "public quote body",
29
+ status: "published",
30
+ });
31
+ await services.posts.create({
32
+ format: "quote",
33
+ quoteText: "A private quote",
34
+ bodyMarkdown: "private quote body",
35
+ status: "published",
36
+ visibility: "private",
37
+ });
38
+ await services.posts.create({
39
+ format: "note",
40
+ bodyMarkdown: "a plain note body",
41
+ status: "published",
42
+ });
43
+ }
44
+
45
+ describe("smart collection page", () => {
46
+ it("renders for a signed-out reader, with title, count, and description", async () => {
47
+ const { app, services } = setup();
48
+ await seedQuotes(services);
49
+ await services.smartCollections.create({
50
+ slug: "quotes",
51
+ title: "Quotes",
52
+ description: "Things worth keeping.",
53
+ selection: { format: "quote" },
54
+ });
55
+
56
+ const res = await app.request("/quotes");
57
+ expect(res.status).toBe(200);
58
+ const html = await res.text();
59
+
60
+ expect(html).toContain("Quotes");
61
+ expect(html).toContain("Things worth keeping.");
62
+ // A written description takes the slot; the conditions — and the way to
63
+ // the archive — stay reachable through the marker beside the title.
64
+ expect(html).not.toContain(">Automatically collects: Quotes<");
65
+ expect(html).toContain('title="Automatically collects: Quotes"');
66
+ expect(html).toContain(
67
+ '<a href="/archive?format=quote" class="collection-page-smart-icon"',
68
+ );
69
+ expect(html).toContain("public quote body");
70
+ expect(html).not.toContain("a plain note body");
71
+ // The archive has a chip bar; this page does not.
72
+ expect(html).not.toContain("archive-filters-chips");
73
+ });
74
+
75
+ it("describes itself by its conditions until a description is written", async () => {
76
+ const { app, services } = setup();
77
+ await seedQuotes(services);
78
+ await services.smartCollections.create({
79
+ slug: "quotes",
80
+ title: "Quotes",
81
+ selection: { format: "quote" },
82
+ });
83
+
84
+ const html = await (await app.request("/quotes")).text();
85
+ expect(html).toContain(">Automatically collects: Quotes<");
86
+ });
87
+
88
+ it("links its conditions to the archive showing the same posts", async () => {
89
+ const { app, services } = setup();
90
+ await seedQuotes(services);
91
+ await services.smartCollections.create({
92
+ slug: "quotes",
93
+ title: "Quotes",
94
+ selection: { format: "quote", media: "none" },
95
+ });
96
+
97
+ const html = await (await app.request("/quotes")).text();
98
+ expect(html).toContain("/archive?format=quote&amp;media=none");
99
+ });
100
+
101
+ it("says so when nothing matches, rather than rendering blank", async () => {
102
+ const { app, services } = setup();
103
+ await seedQuotes(services);
104
+ await services.smartCollections.create({
105
+ slug: "links",
106
+ title: "Links",
107
+ selection: { format: "link" },
108
+ });
109
+
110
+ const html = await (await app.request("/links")).text();
111
+ expect(html).toContain("Nothing matches these conditions yet.");
112
+ });
113
+
114
+ it("collects every post when no condition narrows it", async () => {
115
+ const { app, services } = setup();
116
+ await seedQuotes(services);
117
+ await services.smartCollections.create({
118
+ slug: "everything",
119
+ title: "Everything",
120
+ });
121
+
122
+ const html = await (await app.request("/everything")).text();
123
+ expect(html).toContain("Automatically collects every post.");
124
+ expect(html).toContain("public quote body");
125
+ expect(html).toContain("a plain note body");
126
+ });
127
+
128
+ // The whole point of forbidding private conditions: there is no smart
129
+ // collection page that answers 404 to a reader.
130
+ it("shows the private post to its author and to nobody else", async () => {
131
+ const reader = setup(false);
132
+ await seedQuotes(reader.services);
133
+ await reader.services.smartCollections.create({
134
+ slug: "quotes",
135
+ title: "Quotes",
136
+ selection: { format: "quote" },
137
+ });
138
+ const readerHtml = await (await reader.app.request("/quotes")).text();
139
+ expect(readerHtml).toContain("public quote body");
140
+ expect(readerHtml).not.toContain("private quote body");
141
+ expect(readerHtml).toContain("1 thread");
142
+
143
+ const authorApp = setup(true);
144
+ await seedQuotes(authorApp.services);
145
+ await authorApp.services.smartCollections.create({
146
+ slug: "quotes",
147
+ title: "Quotes",
148
+ selection: { format: "quote" },
149
+ });
150
+ const authorHtml = await (await authorApp.app.request("/quotes")).text();
151
+ expect(authorHtml).toContain("private quote body");
152
+ expect(authorHtml).toContain("2 threads");
153
+ });
154
+
155
+ it("ignores condition parameters in the URL", async () => {
156
+ const { app, services } = setup();
157
+ await seedQuotes(services);
158
+ await services.smartCollections.create({
159
+ slug: "quotes",
160
+ title: "Quotes",
161
+ selection: { format: "quote" },
162
+ });
163
+
164
+ // Membership is edited in the dialog. A reader appending `?format=note`
165
+ // must not be able to redefine what the page collects.
166
+ const html = await (await app.request("/quotes?format=note")).text();
167
+ expect(html).toContain("public quote body");
168
+ expect(html).not.toContain("a plain note body");
169
+ });
170
+
171
+ it("honors ?sort= over the stored default", async () => {
172
+ const { app, services } = setup();
173
+ await services.posts.create({
174
+ format: "quote",
175
+ quoteText: "Older",
176
+ bodyMarkdown: "older body",
177
+ status: "published",
178
+ publishedAt: Date.UTC(2024, 0, 1) / 1000,
179
+ });
180
+ await services.posts.create({
181
+ format: "quote",
182
+ quoteText: "Newer",
183
+ bodyMarkdown: "newer body",
184
+ status: "published",
185
+ publishedAt: Date.UTC(2026, 0, 1) / 1000,
186
+ });
187
+ await services.smartCollections.create({
188
+ slug: "quotes",
189
+ title: "Quotes",
190
+ selection: { format: "quote" },
191
+ sort: "newest",
192
+ });
193
+
194
+ const stored = await (await app.request("/quotes")).text();
195
+ expect(stored.indexOf("newer body")).toBeLessThan(
196
+ stored.indexOf("older body"),
197
+ );
198
+
199
+ const flipped = await (await app.request("/quotes?sort=oldest")).text();
200
+ expect(flipped.indexOf("older body")).toBeLessThan(
201
+ flipped.indexOf("newer body"),
202
+ );
203
+ });
204
+
205
+ it("orders newest by date, not by rating", async () => {
206
+ const { app, services } = setup();
207
+ await services.posts.create({
208
+ format: "quote",
209
+ quoteText: "Older",
210
+ bodyMarkdown: "older body",
211
+ status: "published",
212
+ publishedAt: Date.UTC(2024, 0, 1) / 1000,
213
+ rating: 5,
214
+ });
215
+ await services.posts.create({
216
+ format: "quote",
217
+ quoteText: "Newer",
218
+ bodyMarkdown: "newer body",
219
+ status: "published",
220
+ publishedAt: Date.UTC(2026, 0, 1) / 1000,
221
+ rating: 1,
222
+ });
223
+ await services.smartCollections.create({
224
+ slug: "quotes",
225
+ title: "Quotes",
226
+ selection: { format: "quote" },
227
+ sort: "newest",
228
+ });
229
+
230
+ // The page once handed `newest` to a query that read it as the ascending
231
+ // rating order, which put the worst-rated post on top of a dated page.
232
+ const page = await (await app.request("/quotes")).text();
233
+ expect(page.indexOf("newer body")).toBeLessThan(page.indexOf("older body"));
234
+ });
235
+
236
+ it("still answers a link to the retired ?sort=updated", async () => {
237
+ const { app, services } = setup();
238
+ await services.posts.create({
239
+ format: "quote",
240
+ quoteText: "Older",
241
+ bodyMarkdown: "older body",
242
+ status: "published",
243
+ publishedAt: Date.UTC(2024, 0, 1) / 1000,
244
+ });
245
+ await services.posts.create({
246
+ format: "quote",
247
+ quoteText: "Newer",
248
+ bodyMarkdown: "newer body",
249
+ status: "published",
250
+ publishedAt: Date.UTC(2026, 0, 1) / 1000,
251
+ });
252
+ await services.smartCollections.create({
253
+ slug: "quotes",
254
+ title: "Quotes",
255
+ selection: { format: "quote" },
256
+ // A stored order the legacy value must not fall back to, or the test
257
+ // would pass on the fallback rather than on the mapping.
258
+ sort: "oldest",
259
+ });
260
+
261
+ const legacy = await (await app.request("/quotes?sort=updated")).text();
262
+ expect(legacy.indexOf("newer body")).toBeLessThan(
263
+ legacy.indexOf("older body"),
264
+ );
265
+ });
266
+
267
+ it("404s an address no smart collection holds", async () => {
268
+ const { app } = setup();
269
+ expect((await app.request("/nothing-here")).status).toBe(404);
270
+ });
271
+ });
272
+
273
+ describe("smart collection feed", () => {
274
+ it("serves an anonymous feed with no guard to trip over", async () => {
275
+ const { app, services } = setup();
276
+ await seedQuotes(services);
277
+ await services.smartCollections.create({
278
+ slug: "quotes",
279
+ title: "Quotes",
280
+ selection: { format: "quote" },
281
+ });
282
+
283
+ const res = await app.request("/quotes/feed");
284
+ expect(res.status).toBe(200);
285
+ const xml = await res.text();
286
+
287
+ expect(xml).toContain("A public quote");
288
+ // A feed carries no session, so the private quote can never appear in one.
289
+ expect(xml).not.toContain("A private quote");
290
+ expect(xml).toContain('rel="self"');
291
+ expect(xml).toContain("/quotes/feed");
292
+ });
293
+
294
+ it("404s a feed for an address no smart collection holds", async () => {
295
+ const { app } = setup();
296
+ expect((await app.request("/nothing-here/feed")).status).toBe(404);
297
+ });
298
+ });