@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,89 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { sendDiscoverPing } from "../discover-ping.js";
3
+
4
+ const NOW = 1_800_000_000;
5
+
6
+ /** No real waiting between attempts; the retry itself is what is under test. */
7
+ const options = {
8
+ endpoint: "https://directory.example/api/discover/ping",
9
+ feedUrl: "https://example.com/latest/feed",
10
+ now: () => NOW,
11
+ sleep: async () => {},
12
+ };
13
+
14
+ describe("sendDiscoverPing", () => {
15
+ let fetchMock: ReturnType<typeof vi.fn>;
16
+
17
+ beforeEach(() => {
18
+ fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 202 }));
19
+ vi.stubGlobal("fetch", fetchMock);
20
+ });
21
+
22
+ afterEach(() => {
23
+ vi.unstubAllGlobals();
24
+ vi.restoreAllMocks();
25
+ });
26
+
27
+ it("posts the feed address and nothing else", async () => {
28
+ const outcome = await sendDiscoverPing(options);
29
+
30
+ expect(fetchMock).toHaveBeenCalledTimes(1);
31
+ const [url, init] = fetchMock.mock.calls[0] as [
32
+ string,
33
+ globalThis.RequestInit,
34
+ ];
35
+ expect(url).toBe("https://directory.example/api/discover/ping");
36
+ expect(init.method).toBe("POST");
37
+ expect(JSON.parse(String(init.body))).toEqual({
38
+ feed: "https://example.com/latest/feed",
39
+ });
40
+ expect(outcome).toMatchObject({
41
+ at: NOW,
42
+ ok: true,
43
+ feedUrl: "https://example.com/latest/feed",
44
+ });
45
+ });
46
+
47
+ it("does not retry an announcement that got through", async () => {
48
+ await sendDiscoverPing(options);
49
+
50
+ expect(fetchMock).toHaveBeenCalledTimes(1);
51
+ });
52
+
53
+ // The failure worth retrying automatically: a directory mid-deploy, or one
54
+ // answering a transient 5xx. One more attempt costs nothing and fixes it.
55
+ it("retries once, and reports success when the second attempt lands", async () => {
56
+ fetchMock
57
+ .mockResolvedValueOnce(new Response(null, { status: 503 }))
58
+ .mockResolvedValueOnce(new Response(null, { status: 202 }));
59
+
60
+ const outcome = await sendDiscoverPing(options);
61
+
62
+ expect(fetchMock).toHaveBeenCalledTimes(2);
63
+ expect(outcome.ok).toBe(true);
64
+ expect(outcome.error).toBeUndefined();
65
+ });
66
+
67
+ // The whole point of the rewrite: a lost announcement leaves evidence the
68
+ // owner can read, instead of a line in a log nobody sees.
69
+ it("reports the status a refusing directory answered", async () => {
70
+ fetchMock.mockResolvedValue(new Response(null, { status: 403 }));
71
+
72
+ const outcome = await sendDiscoverPing(options);
73
+
74
+ expect(fetchMock).toHaveBeenCalledTimes(2);
75
+ expect(outcome).toMatchObject({ at: NOW, ok: false, status: 403 });
76
+ expect(outcome.error).toContain("403");
77
+ });
78
+
79
+ it("reports a network failure rather than throwing", async () => {
80
+ fetchMock.mockRejectedValue(new Error("connect ECONNREFUSED"));
81
+
82
+ const outcome = await sendDiscoverPing(options);
83
+
84
+ expect(outcome.ok).toBe(false);
85
+ expect(outcome.error).toContain("ECONNREFUSED");
86
+ // No `status`: nothing answered, so claiming one would be a fabrication.
87
+ expect(outcome.status).toBeUndefined();
88
+ });
89
+ });
@@ -0,0 +1,410 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ discoverIntroRuns,
4
+ getDiscoverFeedPath,
5
+ getDiscoverPageUrls,
6
+ getDiscoverSubmitUrl,
7
+ linkTerms,
8
+ measureDiscoverMaturity,
9
+ parseDiscoverSetting,
10
+ resolveDiscoverMode,
11
+ } from "../discover.js";
12
+ import {
13
+ DEFAULT_DISCOVER_PING_URL,
14
+ getDiscoverDirectoryBaseUrl,
15
+ getDiscoverPingUrl,
16
+ } from "../env.js";
17
+
18
+ function resolve(
19
+ overrides: Partial<Parameters<typeof resolveDiscoverMode>[0]>,
20
+ ) {
21
+ return resolveDiscoverMode({
22
+ storedValue: null,
23
+ defaultValue: null,
24
+ demoMode: false,
25
+ noindex: false,
26
+ rssFeedsEnabled: true,
27
+ ...overrides,
28
+ });
29
+ }
30
+
31
+ describe("parseDiscoverSetting", () => {
32
+ it("accepts the three stored values", () => {
33
+ expect(parseDiscoverSetting("latest")).toBe("latest");
34
+ expect(parseDiscoverSetting("featured")).toBe("featured");
35
+ expect(parseDiscoverSetting("off")).toBe("off");
36
+ });
37
+
38
+ it("treats absent, blank, and unrecognized values as unset", () => {
39
+ expect(parseDiscoverSetting(undefined)).toBeNull();
40
+ expect(parseDiscoverSetting(null)).toBeNull();
41
+ expect(parseDiscoverSetting("")).toBeNull();
42
+ expect(parseDiscoverSetting(" ")).toBeNull();
43
+ expect(parseDiscoverSetting("true")).toBeNull();
44
+ });
45
+ });
46
+
47
+ describe("resolveDiscoverMode", () => {
48
+ // The declaration is the consent record. A self-hosted site nobody
49
+ // configured has said nothing, and a directory must read that as a no.
50
+ it("keeps an untouched self-hosted site out", () => {
51
+ expect(resolve({})).toBe("none");
52
+ });
53
+
54
+ it("honours the owner's stored choice", () => {
55
+ expect(resolve({ storedValue: "featured" })).toBe("featured");
56
+ expect(resolve({ storedValue: "latest" })).toBe("latest");
57
+ expect(resolve({ storedValue: "off" })).toBe("none");
58
+ });
59
+
60
+ // How hosted Jant lists its fleet without asking every owner.
61
+ it("takes the deployment default when nothing is stored", () => {
62
+ expect(resolve({ defaultValue: "latest" })).toBe("latest");
63
+ expect(resolve({ defaultValue: "featured" })).toBe("featured");
64
+ expect(resolve({ defaultValue: "off" })).toBe("none");
65
+ });
66
+
67
+ it("lets the owner overrule the deployment default", () => {
68
+ expect(resolve({ defaultValue: "latest", storedValue: "off" })).toBe(
69
+ "none",
70
+ );
71
+ expect(resolve({ defaultValue: "off", storedValue: "latest" })).toBe(
72
+ "latest",
73
+ );
74
+ });
75
+
76
+ // Hiding from search engines and being surfaced by a directory contradict
77
+ // each other, and the quieter reading is the safe one.
78
+ it("reads noindex as none while the setting is unset", () => {
79
+ expect(resolve({ noindex: true })).toBe("none");
80
+ });
81
+
82
+ // The whole reason the two are separate: a deployment-wide "list my blogs"
83
+ // is a default, not the answer of the owner who hid this one from search.
84
+ it("puts noindex above the deployment default", () => {
85
+ expect(resolve({ noindex: true, defaultValue: "latest" })).toBe("none");
86
+ });
87
+
88
+ it("lets a stored choice override noindex", () => {
89
+ expect(resolve({ noindex: true, storedValue: "latest" })).toBe("latest");
90
+ expect(resolve({ noindex: true, storedValue: "featured" })).toBe(
91
+ "featured",
92
+ );
93
+ });
94
+
95
+ // Demos exist to be thrown away; nothing they publish belongs in a directory.
96
+ it("locks demo sites out even when they ask to be listed", () => {
97
+ expect(resolve({ demoMode: true })).toBe("none");
98
+ expect(resolve({ demoMode: true, storedValue: "featured" })).toBe("none");
99
+ expect(resolve({ demoMode: true, defaultValue: "latest" })).toBe("none");
100
+ });
101
+
102
+ // Every feed path 404s with feeds off, so there would be nothing to poll.
103
+ it("declares none when the site publishes no feeds", () => {
104
+ expect(resolve({ rssFeedsEnabled: false })).toBe("none");
105
+ expect(resolve({ rssFeedsEnabled: false, storedValue: "latest" })).toBe(
106
+ "none",
107
+ );
108
+ });
109
+
110
+ it("ignores values it does not recognize, on either side", () => {
111
+ expect(resolve({ storedValue: "sometimes", defaultValue: "latest" })).toBe(
112
+ "latest",
113
+ );
114
+ expect(resolve({ defaultValue: "sometimes" })).toBe("none");
115
+ });
116
+ });
117
+
118
+ describe("getDiscoverFeedPath", () => {
119
+ it("names the feed each mode draws from", () => {
120
+ expect(getDiscoverFeedPath("latest")).toBe("/latest/feed");
121
+ expect(getDiscoverFeedPath("featured")).toBe("/featured/feed");
122
+ expect(getDiscoverFeedPath("none")).toBeNull();
123
+ });
124
+ });
125
+
126
+ /**
127
+ * The two states that matter are spelled differently, and collapsing them is
128
+ * how "switch the ping off" would quietly become "switch it on".
129
+ */
130
+ describe("getDiscoverPingUrl", () => {
131
+ it("uses Jant's directory when nothing is configured", () => {
132
+ expect(getDiscoverPingUrl({})).toBe(DEFAULT_DISCOVER_PING_URL);
133
+ expect(getDiscoverPingUrl(undefined)).toBe(DEFAULT_DISCOVER_PING_URL);
134
+ });
135
+
136
+ it("announces nowhere when the binding is set but empty", () => {
137
+ expect(getDiscoverPingUrl({ DISCOVER_PING_URL: "" })).toBeUndefined();
138
+ expect(getDiscoverPingUrl({ DISCOVER_PING_URL: " " })).toBeUndefined();
139
+ });
140
+
141
+ it("uses a directory of the operator's own", () => {
142
+ expect(
143
+ getDiscoverPingUrl({
144
+ DISCOVER_PING_URL: "https://directory.example/api/discover/ping",
145
+ }),
146
+ ).toBe("https://directory.example/api/discover/ping");
147
+ });
148
+
149
+ // Naming the directory twice is what let a hosted deployment announce its
150
+ // blogs to jant.me, which had never heard of them.
151
+ it("announces to the control plane that hosts this deployment", () => {
152
+ expect(
153
+ getDiscoverPingUrl({
154
+ HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
155
+ }),
156
+ ).toBe("https://cloud.example/api/discover/ping");
157
+ });
158
+
159
+ // Server-to-server, like every other core to control-plane call.
160
+ it("prefers the control plane's internal address", () => {
161
+ expect(
162
+ getDiscoverPingUrl({
163
+ HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
164
+ HOSTED_CONTROL_PLANE_INTERNAL_BASE_URL: "http://127.0.0.1:3300",
165
+ }),
166
+ ).toBe("http://127.0.0.1:3300/api/discover/ping");
167
+ });
168
+
169
+ it("lets an explicit directory override the control plane", () => {
170
+ expect(
171
+ getDiscoverPingUrl({
172
+ DISCOVER_PING_URL: "https://directory.example/api/discover/ping",
173
+ HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
174
+ }),
175
+ ).toBe("https://directory.example/api/discover/ping");
176
+ });
177
+
178
+ it("still announces nowhere when a hosted deployment empties the binding", () => {
179
+ expect(
180
+ getDiscoverPingUrl({
181
+ DISCOVER_PING_URL: "",
182
+ HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
183
+ }),
184
+ ).toBeUndefined();
185
+ });
186
+
187
+ it("falls back to Jant's directory when the control plane URL is unusable", () => {
188
+ expect(
189
+ getDiscoverPingUrl({ HOSTED_CONTROL_PLANE_BASE_URL: "not a url" }),
190
+ ).toBe(DEFAULT_DISCOVER_PING_URL);
191
+ });
192
+ });
193
+
194
+ /**
195
+ * The address a browser opens, for the same directory the ping goes to. The
196
+ * two must never name different directories, which is why they share a source.
197
+ */
198
+ describe("getDiscoverDirectoryBaseUrl", () => {
199
+ it("uses Jant's directory when nothing is configured", () => {
200
+ expect(getDiscoverDirectoryBaseUrl({})).toBe("https://jant.me/");
201
+ });
202
+
203
+ // The public address, not the internal one the ping uses: this ends up in an
204
+ // href, and `127.0.0.1` is not a directory anybody can visit.
205
+ it("links to the control plane's public address", () => {
206
+ expect(
207
+ getDiscoverDirectoryBaseUrl({
208
+ HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
209
+ HOSTED_CONTROL_PLANE_INTERNAL_BASE_URL: "http://127.0.0.1:3300",
210
+ }),
211
+ ).toBe("https://cloud.example/");
212
+ });
213
+
214
+ it("follows an explicitly configured directory", () => {
215
+ expect(
216
+ getDiscoverDirectoryBaseUrl({
217
+ DISCOVER_PING_URL: "https://directory.example/api/discover/ping",
218
+ }),
219
+ ).toBe("https://directory.example/");
220
+ });
221
+
222
+ it("has nothing to link to when announcing is off", () => {
223
+ expect(
224
+ getDiscoverDirectoryBaseUrl({ DISCOVER_PING_URL: "" }),
225
+ ).toBeUndefined();
226
+ });
227
+ });
228
+
229
+ describe("measureDiscoverMaturity", () => {
230
+ it("passes a blog that has published something", () => {
231
+ expect(measureDiscoverMaturity({ publicPostCount: 5 })).toEqual({
232
+ publicPostCount: 5,
233
+ established: true,
234
+ });
235
+ });
236
+
237
+ // One post is the whole threshold; what keeps throwaways out is the opt-in.
238
+ it("passes a blog with a single public post", () => {
239
+ expect(measureDiscoverMaturity({ publicPostCount: 1 })).toMatchObject({
240
+ established: true,
241
+ });
242
+ });
243
+
244
+ it("holds back a blog with nothing public in it", () => {
245
+ expect(measureDiscoverMaturity({ publicPostCount: 0 })).toEqual({
246
+ publicPostCount: 0,
247
+ established: false,
248
+ });
249
+ });
250
+ });
251
+
252
+ describe("getDiscoverSubmitUrl", () => {
253
+ it("finds the form beside the directory's own ping endpoint", () => {
254
+ expect(getDiscoverSubmitUrl("https://jant.me/api/discover/ping")).toBe(
255
+ "https://jant.me/discover/submit",
256
+ );
257
+ });
258
+
259
+ // Provider-neutral on purpose: a site announcing to its own directory must
260
+ // not be sent to somebody else's form.
261
+ it("follows a directory of your own", () => {
262
+ expect(
263
+ getDiscoverSubmitUrl("https://directory.example/api/discover/ping"),
264
+ ).toBe("https://directory.example/discover/submit");
265
+ });
266
+
267
+ it("has nothing to offer when no directory is configured", () => {
268
+ expect(getDiscoverSubmitUrl(undefined)).toBeNull();
269
+ expect(getDiscoverSubmitUrl("")).toBeNull();
270
+ expect(getDiscoverSubmitUrl("not a url")).toBeNull();
271
+ });
272
+ });
273
+
274
+ describe("getDiscoverPageUrls", () => {
275
+ // The settings page links the directory's name to the directory itself, so
276
+ // "what is Discover" is answered by the list rather than by a page about it.
277
+ // The three lists sit flat on jant.me; only the rules sit under /discover.
278
+ it("finds the directory's pages behind its own ping endpoint", () => {
279
+ expect(getDiscoverPageUrls("https://jant.me/api/discover/ping")).toEqual({
280
+ home: "https://jant.me/discover",
281
+ links: "https://jant.me/links",
282
+ quotes: "https://jant.me/quotes",
283
+ rules: "https://jant.me/discover/about",
284
+ });
285
+ });
286
+
287
+ it("follows a directory of your own", () => {
288
+ expect(
289
+ getDiscoverPageUrls("https://directory.example/api/discover/ping"),
290
+ ).toEqual({
291
+ home: "https://directory.example/discover",
292
+ links: "https://directory.example/links",
293
+ quotes: "https://directory.example/quotes",
294
+ rules: "https://directory.example/discover/about",
295
+ });
296
+ });
297
+
298
+ it("has nothing to link to when no directory is configured", () => {
299
+ expect(getDiscoverPageUrls(undefined)).toBeNull();
300
+ expect(getDiscoverPageUrls("")).toBeNull();
301
+ expect(getDiscoverPageUrls("not a url")).toBeNull();
302
+ });
303
+ });
304
+
305
+ describe("linkTerms", () => {
306
+ const links = [
307
+ { term: "Jant Discover", href: "/discover" },
308
+ { term: "Links", href: "/links" },
309
+ { term: "Quotes", href: "/quotes" },
310
+ { term: "Discover community rules", href: "/discover/about" },
311
+ ];
312
+
313
+ it("links every term where the sentence puts it", () => {
314
+ expect(
315
+ linkTerms(
316
+ "Jant Discover lists link and quote posts on Links and Quotes. See the Discover community rules.",
317
+ links,
318
+ ),
319
+ ).toEqual([
320
+ { text: "Jant Discover", href: "/discover" },
321
+ { text: " lists link and quote posts on " },
322
+ { text: "Links", href: "/links" },
323
+ { text: " and " },
324
+ { text: "Quotes", href: "/quotes" },
325
+ { text: ". See the " },
326
+ { text: "Discover community rules", href: "/discover/about" },
327
+ { text: "." },
328
+ ]);
329
+ });
330
+
331
+ // A translation owns its word order; the links follow the words, not the
332
+ // order they were given in.
333
+ it("follows the translation's word order", () => {
334
+ expect(
335
+ linkTerms("详见 Discover 社区规则。Jant Discover 是一个目录。", [
336
+ { term: "Jant Discover", href: "/discover" },
337
+ { term: "Discover 社区规则", href: "/discover/about" },
338
+ ]),
339
+ ).toEqual([
340
+ { text: "详见 " },
341
+ { text: "Discover 社区规则", href: "/discover/about" },
342
+ { text: "。" },
343
+ { text: "Jant Discover", href: "/discover" },
344
+ { text: " 是一个目录。" },
345
+ ]);
346
+ });
347
+
348
+ // "Discover" alone sits inside "Jant Discover"; whichever the sentence
349
+ // mentions first, the longer name keeps its whole run.
350
+ it("never lets a term inside another take the longer one's place", () => {
351
+ expect(
352
+ linkTerms("Jant Discover reads your feed. Discover shows it.", [
353
+ { term: "Discover", href: "/about" },
354
+ { term: "Jant Discover", href: "/discover" },
355
+ ]),
356
+ ).toEqual([
357
+ { text: "Jant Discover", href: "/discover" },
358
+ { text: " reads your feed. " },
359
+ { text: "Discover", href: "/about" },
360
+ { text: " shows it." },
361
+ ]);
362
+ });
363
+
364
+ it("leaves a term the translation dropped as plain text", () => {
365
+ expect(
366
+ linkTerms("A directory of Jant blogs.", [
367
+ { term: "Jant Discover", href: "/discover" },
368
+ ]),
369
+ ).toEqual([{ text: "A directory of Jant blogs." }]);
370
+ });
371
+
372
+ it("links nothing for an empty term or address", () => {
373
+ expect(
374
+ linkTerms("See Links.", [
375
+ { term: "", href: "/links" },
376
+ { term: "Links", href: "" },
377
+ ]),
378
+ ).toEqual([{ text: "See Links." }]);
379
+ });
380
+ });
381
+
382
+ describe("discoverIntroRuns", () => {
383
+ const terms = { name: "Jant Discover", rules: "Discover community rules" };
384
+ const intro =
385
+ "Jant Discover lists link and quote posts on Links and Quotes. See the Discover community rules.";
386
+
387
+ // The settings page and the setup screen both render the line through here,
388
+ // so this is the one place that says which words go where.
389
+ it("links the name, both lists and the rules to their pages", () => {
390
+ expect(
391
+ discoverIntroRuns(intro, terms, getDiscoverPageUrls("https://jant.me/")),
392
+ ).toEqual([
393
+ { text: "Jant Discover", href: "https://jant.me/discover" },
394
+ { text: " lists link and quote posts on " },
395
+ { text: "Links", href: "https://jant.me/links" },
396
+ { text: " and " },
397
+ { text: "Quotes", href: "https://jant.me/quotes" },
398
+ { text: ". See the " },
399
+ {
400
+ text: "Discover community rules",
401
+ href: "https://jant.me/discover/about",
402
+ },
403
+ { text: "." },
404
+ ]);
405
+ });
406
+
407
+ it("leaves the whole line plain when there is no directory", () => {
408
+ expect(discoverIntroRuns(intro, terms, null)).toEqual([{ text: intro }]);
409
+ });
410
+ });
@@ -1,9 +1,35 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import {
3
+ FEED_LIMIT_MAX,
3
4
  getFeedEntryUpdatedAt,
4
5
  getRssPublishedBefore,
6
+ parseFeedLimit,
5
7
  } from "../feed-policy.js";
6
8
 
9
+ describe("parseFeedLimit", () => {
10
+ it("keeps the site's length when the parameter is absent", () => {
11
+ expect(parseFeedLimit(undefined, 50)).toBe(50);
12
+ });
13
+
14
+ it("honours a request above or below the site's length", () => {
15
+ expect(parseFeedLimit("200", 50)).toBe(200);
16
+ expect(parseFeedLimit("5", 50)).toBe(5);
17
+ expect(parseFeedLimit("050", 50)).toBe(50);
18
+ });
19
+
20
+ it("gives the most there is to a request past the ceiling", () => {
21
+ expect(parseFeedLimit(String(FEED_LIMIT_MAX), 50)).toBe(FEED_LIMIT_MAX);
22
+ expect(parseFeedLimit(String(FEED_LIMIT_MAX + 1), 50)).toBe(FEED_LIMIT_MAX);
23
+ expect(parseFeedLimit("99999999999999999999", 50)).toBe(FEED_LIMIT_MAX);
24
+ });
25
+
26
+ it("ignores anything that is not a positive whole number", () => {
27
+ for (const raw of ["", "0", "-3", "1.5", "abc", "10abc", " 10", "1e3"]) {
28
+ expect(parseFeedLimit(raw, 50)).toBe(50);
29
+ }
30
+ });
31
+ });
32
+
7
33
  describe("feed policy", () => {
8
34
  it("makes the exact delay boundary eligible", () => {
9
35
  expect(getRssPublishedBefore(300, 1_000)).toBe(701);