@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,329 @@
1
+ /**
2
+ * A smart collection on an exported site lists what it lists on Jant.
3
+ *
4
+ * The export carries a smart collection's conditions, not its matches, and
5
+ * the theme's `smart-collection-members` partial evaluates them against the
6
+ * posts' front matter. That is a second implementation of every condition and
7
+ * every order, in Go templates, so this builds the export with the real Hugo
8
+ * binary and compares each page with the list Jant's own services produce
9
+ * from the same database.
10
+ *
11
+ * Skips when `hugo` is not on PATH, like the other Hugo build tests.
12
+ */
13
+
14
+ import { rm } from "node:fs/promises";
15
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
16
+ import { createTestDatabase, DEFAULT_TEST_SITE_ID } from "./helpers/db.js";
17
+ import { makeSiteConfig } from "./helpers/export-fixtures.js";
18
+ import {
19
+ buildHugoSite,
20
+ hugoAvailable,
21
+ readFeedSlugs,
22
+ readThreadSlugs,
23
+ } from "./helpers/hugo-site.js";
24
+ import { createCollectionService } from "../services/collection.js";
25
+ import { createExportService } from "../services/export.js";
26
+ import { createMediaService } from "../services/media.js";
27
+ import { createPathService } from "../services/path.js";
28
+ import { createPostService } from "../services/post.js";
29
+ import { createSmartCollectionService } from "../services/smart-collection.js";
30
+ import { supportsCollectionRatingSort } from "../lib/collection-sort.js";
31
+ import type { Database } from "../db/index.js";
32
+ import type { CreatePost } from "../types/operations.js";
33
+ import type { MediaKind, SmartCollection } from "../types.js";
34
+
35
+ const SITE_URL = "https://example.com";
36
+
37
+ const at = (iso: string) => Math.floor(Date.parse(iso) / 1000);
38
+
39
+ /** Every condition value and every order, each on its own smart collection. */
40
+ const SMART_COLLECTIONS: {
41
+ slug: string;
42
+ selection: SmartCollection["selection"] | "ideas";
43
+ sort?: SmartCollection["sort"];
44
+ }[] = [
45
+ { slug: "sc-all-newest", selection: {} },
46
+ { slug: "sc-all-oldest", selection: {}, sort: "oldest" },
47
+ { slug: "sc-all-rated", selection: {}, sort: "rating_desc" },
48
+ { slug: "sc-notes", selection: { format: "note" } },
49
+ // One rated link of two: too few to rank, so the page falls back to newest.
50
+ { slug: "sc-links", selection: { format: "link" }, sort: "rating_desc" },
51
+ { slug: "sc-quotes", selection: { format: "quote" } },
52
+ { slug: "sc-titled", selection: { title: true } },
53
+ { slug: "sc-untitled", selection: { title: false } },
54
+ { slug: "sc-2024", selection: { year: 2024 } },
55
+ { slug: "sc-2025", selection: { year: 2025 } },
56
+ { slug: "sc-media", selection: { media: "any" } },
57
+ { slug: "sc-no-media", selection: { media: "none" } },
58
+ { slug: "sc-images", selection: { media: ["image"] } },
59
+ { slug: "sc-text-or-video", selection: { media: ["text", "video"] } },
60
+ { slug: "sc-replies", selection: { replies: true } },
61
+ { slug: "sc-no-replies", selection: { replies: false } },
62
+ { slug: "sc-public", selection: { visibility: "public" } },
63
+ { slug: "sc-hidden", selection: { visibility: "latest_hidden" } },
64
+ { slug: "sc-featured", selection: { visibility: "featured" } },
65
+ { slug: "sc-ideas", selection: "ideas" },
66
+ {
67
+ slug: "sc-thoughts",
68
+ selection: { format: "note", title: false },
69
+ sort: "oldest",
70
+ },
71
+ ];
72
+
73
+ describe.skipIf(!hugoAvailable)("smart collections on an exported site", () => {
74
+ let siteDir: string;
75
+ const expected = new Map<string, string[]>();
76
+ const built = new Map<string, string[]>();
77
+ const expectedFeed = new Map<string, string[]>();
78
+ const builtFeed = new Map<string, string[]>();
79
+
80
+ beforeAll(async () => {
81
+ const { db: testDb } = createTestDatabase();
82
+ const db = testDb as unknown as Database;
83
+ const siteId = DEFAULT_TEST_SITE_ID;
84
+ const paths = createPathService(db, siteId);
85
+ const posts = createPostService(db, { slugIdLength: 5 }, siteId);
86
+ const smartCollections = createSmartCollectionService(
87
+ db,
88
+ siteId,
89
+ paths,
90
+ posts,
91
+ );
92
+ const collections = createCollectionService(
93
+ db,
94
+ siteId,
95
+ paths,
96
+ undefined,
97
+ undefined,
98
+ smartCollections,
99
+ );
100
+ const media = createMediaService(db, siteId);
101
+
102
+ const ideas = await collections.create({ slug: "ideas", title: "Ideas" });
103
+
104
+ async function post(
105
+ slug: string,
106
+ data: Omit<CreatePost, "slug" | "bodyMarkdown">,
107
+ attachments: MediaKind[] = [],
108
+ ) {
109
+ const created = await posts.create({
110
+ slug,
111
+ bodyMarkdown: `Body of ${slug}.`,
112
+ status: "published",
113
+ ...data,
114
+ createdAt: data.createdAt ?? data.publishedAt,
115
+ });
116
+ for (const [index, kind] of attachments.entries()) {
117
+ await media.create({
118
+ postId: created.id,
119
+ filename: `${slug}-${index}.bin`,
120
+ originalName: `${slug}-${index}.bin`,
121
+ mimeType: "application/octet-stream",
122
+ size: 1,
123
+ storageKey: `media/${slug}-${index}.bin`,
124
+ mediaKind: kind,
125
+ });
126
+ }
127
+ return created;
128
+ }
129
+
130
+ await post("a-2024-note", {
131
+ format: "note",
132
+ publishedAt: at("2024-03-01T00:00:00Z"),
133
+ rating: 5,
134
+ });
135
+ const titled = await post(
136
+ "b-titled-note",
137
+ {
138
+ format: "note",
139
+ title: "Titled note",
140
+ publishedAt: at("2025-01-10T00:00:00Z"),
141
+ collectionIds: [ideas.id],
142
+ },
143
+ ["image"],
144
+ );
145
+ await post("b-reply", {
146
+ format: "note",
147
+ replyToId: titled.id,
148
+ publishedAt: at("2025-06-01T00:00:00Z"),
149
+ });
150
+ await post(
151
+ "c-hidden-link",
152
+ {
153
+ format: "link",
154
+ title: "A link",
155
+ url: "https://example.org/",
156
+ visibility: "latest_hidden",
157
+ publishedAt: at("2025-02-01T00:00:00Z"),
158
+ rating: 3,
159
+ },
160
+ ["video"],
161
+ );
162
+ await post("k-newer-link", {
163
+ format: "link",
164
+ title: "Another link",
165
+ url: "https://example.net/",
166
+ publishedAt: at("2025-05-01T00:00:00Z"),
167
+ });
168
+ await post("d-featured-quote", {
169
+ format: "quote",
170
+ title: "Seneca",
171
+ quoteText: "We suffer more in imagination than in reality.",
172
+ publishedAt: at("2023-05-05T00:00:00Z"),
173
+ featuredAt: at("2023-06-01T00:00:00Z"),
174
+ });
175
+ await post(
176
+ "e-untitled-quote",
177
+ {
178
+ format: "quote",
179
+ quoteText: "Unattributed.",
180
+ publishedAt: at("2025-03-01T00:00:00Z"),
181
+ collectionIds: [ideas.id],
182
+ },
183
+ ["text"],
184
+ );
185
+ await post("f-private", {
186
+ format: "note",
187
+ visibility: "private",
188
+ publishedAt: at("2025-01-01T00:00:00Z"),
189
+ });
190
+ await post("g-draft", { format: "note", status: "draft" });
191
+ // Two roots published in the same second: the ID decides.
192
+ const tied = await post("h-tied", {
193
+ format: "note",
194
+ publishedAt: at("2025-04-01T12:00:00Z"),
195
+ rating: 4,
196
+ });
197
+ await post("h-draft-reply", {
198
+ format: "note",
199
+ replyToId: tied.id,
200
+ status: "draft",
201
+ });
202
+ await post("i-tied", {
203
+ format: "note",
204
+ publishedAt: at("2025-04-01T12:00:00Z"),
205
+ });
206
+ // The last second of 2024 in UTC, whatever the build machine's zone.
207
+ await post(
208
+ "j-new-years-eve",
209
+ { format: "note", publishedAt: at("2024-12-31T23:59:59Z") },
210
+ ["audio"],
211
+ );
212
+
213
+ const anonymous = { isAuthenticated: false };
214
+ for (const definition of SMART_COLLECTIONS) {
215
+ const smartCollection = await smartCollections.create({
216
+ slug: definition.slug,
217
+ title: definition.slug,
218
+ selection:
219
+ definition.selection === "ideas"
220
+ ? { collection: [ideas.id] }
221
+ : definition.selection,
222
+ sort: definition.sort,
223
+ });
224
+ // What the page shows: the stored order, unless it ranks by rating
225
+ // and too little is rated for that to mean anything.
226
+ const rated = await posts.countUpTo(
227
+ {
228
+ ...smartCollections.toPostFilters(smartCollection, anonymous),
229
+ hasRating: true,
230
+ },
231
+ 2,
232
+ );
233
+ const sort =
234
+ smartCollection.sort === "rating_desc" &&
235
+ !supportsCollectionRatingSort(rated)
236
+ ? "newest"
237
+ : smartCollection.sort;
238
+ const list = await posts.list({
239
+ ...smartCollections.toPostFilters(
240
+ { ...smartCollection, sort },
241
+ anonymous,
242
+ ),
243
+ limit: 1000,
244
+ });
245
+ expected.set(
246
+ definition.slug,
247
+ list.map((item) => item.slug),
248
+ );
249
+ // The feed keeps the stored order, fallback or not.
250
+ const feedList = await posts.list({
251
+ ...smartCollections.toPostFilters(smartCollection, anonymous),
252
+ limit: 50,
253
+ });
254
+ expectedFeed.set(
255
+ definition.slug,
256
+ feedList.map((item) => item.slug),
257
+ );
258
+ }
259
+
260
+ // A small page size, so the pages are read across pagination.
261
+ siteDir = await buildHugoSite(
262
+ await createExportService(
263
+ { posts, paths, collections, media },
264
+ makeSiteConfig({ siteUrl: SITE_URL, pageSize: 4 }),
265
+ { storage: null, bundleMedia: false },
266
+ ).generateHugoFiles(),
267
+ );
268
+ for (const { slug } of SMART_COLLECTIONS) {
269
+ built.set(slug, await readThreadSlugs(siteDir, slug));
270
+ builtFeed.set(slug, await readFeedSlugs(siteDir, slug, SITE_URL));
271
+ }
272
+ }, 60_000);
273
+
274
+ afterAll(async () => {
275
+ if (siteDir) await rm(siteDir, { recursive: true, force: true });
276
+ });
277
+
278
+ it.each(SMART_COLLECTIONS.map(({ slug }) => slug))(
279
+ "%s lists the posts Jant lists, in its order",
280
+ (slug) => {
281
+ expect(built.get(slug)).toEqual(expected.get(slug));
282
+ },
283
+ );
284
+
285
+ // Equal lists prove nothing if the fixture leaves them empty or alike.
286
+ it("runs on a fixture that tells the conditions apart", () => {
287
+ expect(expected.get("sc-thoughts")).toEqual([
288
+ "a-2024-note",
289
+ "j-new-years-eve",
290
+ "h-tied",
291
+ "i-tied",
292
+ ]);
293
+ expect(expected.get("sc-all-rated")?.slice(0, 3)).toEqual([
294
+ "a-2024-note",
295
+ "h-tied",
296
+ "c-hidden-link",
297
+ ]);
298
+ expect(expected.get("sc-links")).toEqual(["k-newer-link", "c-hidden-link"]);
299
+ // The page falls back to newest with one rated link; the feed doesn't.
300
+ expect(expectedFeed.get("sc-links")).toEqual([
301
+ "c-hidden-link",
302
+ "k-newer-link",
303
+ ]);
304
+ expect(expected.get("sc-titled")).toEqual([
305
+ "b-titled-note",
306
+ "k-newer-link",
307
+ "c-hidden-link",
308
+ "d-featured-quote",
309
+ ]);
310
+ expect(expected.get("sc-2024")).toEqual(["j-new-years-eve", "a-2024-note"]);
311
+ expect(expected.get("sc-replies")).toEqual(["b-titled-note"]);
312
+ expect(expected.get("sc-text-or-video")).toEqual([
313
+ "e-untitled-quote",
314
+ "c-hidden-link",
315
+ ]);
316
+ expect(expected.get("sc-all-newest")).not.toContain("f-private");
317
+ expect(expected.get("sc-all-newest")).not.toContain("g-draft");
318
+ for (const { slug } of SMART_COLLECTIONS) {
319
+ expect(expected.get(slug)?.length, slug).toBeGreaterThan(0);
320
+ }
321
+ });
322
+
323
+ it.each(SMART_COLLECTIONS.map(({ slug }) => slug))(
324
+ "%s feed carries what Jant's feed carries, in its order",
325
+ (slug) => {
326
+ expect(builtFeed.get(slug)).toEqual(expectedFeed.get(slug));
327
+ },
328
+ );
329
+ });
@@ -0,0 +1,91 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ /**
5
+ * `src/styles`, resolved from the working directory rather than
6
+ * `import.meta.url`: under the `happy-dom` environment `import.meta.url` is an
7
+ * `http:` URL that `readFileSync` rejects, and Vitest runs with the package
8
+ * root as its working directory in every environment.
9
+ */
10
+ const STYLES_DIR = resolve(process.cwd(), "src/styles");
11
+
12
+ /** Hand-written component CSS a signed-out visitor downloads. */
13
+ const READER_SHEETS = ["ui.css", "components.css"] as const;
14
+
15
+ /** Hand-written component CSS that only loads on an authenticated page. */
16
+ const AUTHOR_SHEETS = ["ui-author.css", "components-author.css"] as const;
17
+
18
+ function read(names: readonly string[]): string {
19
+ return names
20
+ .map((name) => readFileSync(resolve(STYLES_DIR, name), "utf8"))
21
+ .join("\n");
22
+ }
23
+
24
+ /**
25
+ * Every `@layer components` rule Jant hand-writes, both audiences together.
26
+ *
27
+ * `ui.css`/`components.css` carry what readers need; `ui-author.css` and
28
+ * `components-author.css` carry the composer, editor, settings, config editor
29
+ * and dash chrome that only load once someone is signed in. A test asserting
30
+ * that a rule is declared cares that it exists at all, not which half it
31
+ * landed in — so moving a rule between them must never break a test.
32
+ *
33
+ * @returns The concatenated contents of all four stylesheets.
34
+ * @example
35
+ * expect(readComponentCss()).toMatch(/\.compose-post-meta-panel\s*\{/);
36
+ */
37
+ export function readComponentCss(): string {
38
+ return read([...READER_SHEETS, ...AUTHOR_SHEETS]);
39
+ }
40
+
41
+ /**
42
+ * Only the reader halves.
43
+ *
44
+ * Use when the point of the assertion is that a rule ships to signed-out
45
+ * visitors — or, with a negative matcher, that it does not.
46
+ *
47
+ * @returns The concatenated contents of `ui.css` and `components.css`.
48
+ * @example
49
+ * expect(readReaderCss()).not.toMatch(/\.compose-dialog\s*\{/);
50
+ */
51
+ export function readReaderCss(): string {
52
+ return read(READER_SHEETS);
53
+ }
54
+
55
+ /**
56
+ * Only the author halves.
57
+ *
58
+ * @returns The concatenated contents of `ui-author.css` and
59
+ * `components-author.css`.
60
+ * @example
61
+ * expect(readAuthorCss()).toMatch(/\.settings-root\s*\{/);
62
+ */
63
+ export function readAuthorCss(): string {
64
+ return read(AUTHOR_SHEETS);
65
+ }
66
+
67
+ /**
68
+ * Every stylesheet Jant hand-writes, each paired with its filename.
69
+ *
70
+ * `readComponentCss()` and friends concatenate, which is right for asking
71
+ * whether a rule exists. This one keeps the files apart so a failure can name
72
+ * the file and line it found the problem on — `tokens.css` and `preset.css`
73
+ * are in here too, because breakpoints live in all six.
74
+ *
75
+ * @returns One entry per stylesheet, in load order.
76
+ * @example
77
+ * for (const { name, css } of readAllStylesheets()) {
78
+ * expect(css, name).not.toMatch(/@media \(max-width: 767px\)/);
79
+ * }
80
+ */
81
+ export function readAllStylesheets(): readonly {
82
+ name: string;
83
+ css: string;
84
+ }[] {
85
+ return [
86
+ { name: "preset.css", path: resolve(process.cwd(), "src/preset.css") },
87
+ ...[...READER_SHEETS, ...AUTHOR_SHEETS, "tokens.css", "site-media.css"].map(
88
+ (name) => ({ name, path: resolve(STYLES_DIR, name) }),
89
+ ),
90
+ ].map(({ name, path }) => ({ name, css: readFileSync(path, "utf8") }));
91
+ }
@@ -134,3 +134,36 @@ export function createTestDatabase(options?: { fts?: boolean }) {
134
134
 
135
135
  return { db, sqlite };
136
136
  }
137
+
138
+ /** The most bound parameters Cloudflare D1 accepts in one statement. */
139
+ export const D1_MAX_BOUND_PARAMETERS = 100;
140
+
141
+ /**
142
+ * Make a test database refuse what D1 refuses: a statement with more than
143
+ * {@link D1_MAX_BOUND_PARAMETERS} bound parameters.
144
+ *
145
+ * better-sqlite3 accepts tens of thousands, so a query that puts every id in
146
+ * one `IN (…)` list passes every test and fails in production once the list
147
+ * grows. Install this after seeding, so only the code under test is held to it.
148
+ *
149
+ * @param sqlite - The handle `createTestDatabase` or `createTestApp` returned
150
+ * @returns Nothing; the handle is changed in place
151
+ * @example
152
+ * ```ts
153
+ * const { app, sqlite } = createTestApp();
154
+ * await seed(app);
155
+ * enforceD1BoundParameterLimit(sqlite);
156
+ * ```
157
+ */
158
+ export function enforceD1BoundParameterLimit(sqlite: Database.Database): void {
159
+ const prepare = sqlite.prepare.bind(sqlite);
160
+ sqlite.prepare = ((source: string) => {
161
+ const count = source.match(/\?/g)?.length ?? 0;
162
+ if (count > D1_MAX_BOUND_PARAMETERS) {
163
+ throw new Error(
164
+ `too many SQL variables: ${count} bound parameters, D1 allows ${D1_MAX_BOUND_PARAMETERS}`,
165
+ );
166
+ }
167
+ return prepare(source);
168
+ }) as Database.Database["prepare"];
169
+ }
@@ -87,10 +87,13 @@ export function makeSiteConfig(over: Partial<SiteConfig> = {}): SiteConfig {
87
87
  siteUrl: "https://example.com",
88
88
  siteDescription: "Export fixture",
89
89
  siteLanguage: "en",
90
+ multilingualEnabled: false,
91
+ additionalLanguages: [],
90
92
  showJantBrandingOnHome: true,
91
93
  publicApiEnabled: true,
92
94
  rssFeedsEnabled: true,
93
95
  mainRssFeed: "latest",
96
+ archiveDefaultLayout: "list",
94
97
  siteFooter: "",
95
98
  showHeaderAvatar: false,
96
99
  siteAvatarUrl: "",
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Build an export with the real Hugo binary and read lists back out of it.
3
+ *
4
+ * For the tests that hold an exported page to the list Jant shows: the theme
5
+ * reimplements membership and order in Go templates, and only a real build
6
+ * shows what a reader of the static site gets.
7
+ */
8
+
9
+ import { spawn, spawnSync } from "node:child_process";
10
+ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
11
+ import { tmpdir } from "node:os";
12
+ import { dirname, join } from "node:path";
13
+ import { isStoredExportFile, type ExportFile } from "../../services/export.js";
14
+
15
+ /** Whether `hugo` is on PATH. Tests that need it skip without it. */
16
+ export const hugoAvailable =
17
+ spawnSync("hugo", ["version"], { stdio: "ignore" }).status === 0;
18
+
19
+ /**
20
+ * Write an export to a temporary directory and build it, unminified so the
21
+ * markup reads back predictably.
22
+ *
23
+ * @param files - What `generateHugoFiles` returned; stored media is skipped
24
+ * @returns The site directory; the build is in its `public/`
25
+ * @throws {Error} With Hugo's output when the build fails
26
+ * @example
27
+ * const siteDir = await buildHugoSite(await exporter.generateHugoFiles());
28
+ */
29
+ export async function buildHugoSite(files: ExportFile[]): Promise<string> {
30
+ const siteDir = await mkdtemp(join(tmpdir(), "jant-hugo-site-"));
31
+ for (const file of files) {
32
+ if (isStoredExportFile(file)) continue;
33
+ const target = join(siteDir, file.path);
34
+ await mkdir(dirname(target), { recursive: true });
35
+ await writeFile(target, file.content);
36
+ }
37
+
38
+ const { code, log } = await new Promise<{ code: number; log: string }>(
39
+ (resolve) => {
40
+ const child = spawn("hugo", ["--source", siteDir], {
41
+ stdio: ["ignore", "pipe", "pipe"],
42
+ });
43
+ let output = "";
44
+ child.stdout?.on("data", (chunk) => (output += chunk.toString()));
45
+ child.stderr?.on("data", (chunk) => (output += chunk.toString()));
46
+ child.on("close", (exitCode) =>
47
+ resolve({ code: exitCode ?? -1, log: output }),
48
+ );
49
+ },
50
+ );
51
+ if (code !== 0) throw new Error(`Hugo build failed:\n${log}`);
52
+ return siteDir;
53
+ }
54
+
55
+ /**
56
+ * The Thread roots a collection-style page lists, in order, across all of
57
+ * its pages.
58
+ *
59
+ * @param siteDir - From {@link buildHugoSite}
60
+ * @param section - The section's slug
61
+ * @returns Root slugs, first page first
62
+ * @example
63
+ * await readThreadSlugs(siteDir, "ideas"); // ["newest-thread", ...]
64
+ */
65
+ export async function readThreadSlugs(
66
+ siteDir: string,
67
+ section: string,
68
+ ): Promise<string[]> {
69
+ const slugs: string[] = [];
70
+ for (let page = 1; ; page++) {
71
+ const path =
72
+ page === 1
73
+ ? join(siteDir, "public", section, "index.html")
74
+ : join(siteDir, "public", section, "page", String(page), "index.html");
75
+ const html = await readFile(path, "utf8").catch(() => null);
76
+ if (html === null) return slugs;
77
+ for (const match of html.matchAll(
78
+ /<div class="thread thread-full[^"]*" data-slug="([^"]+)"/g,
79
+ )) {
80
+ slugs.push(match[1] as string);
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * The posts a section's Atom feed carries, in order, by slug.
87
+ *
88
+ * @param siteDir - From {@link buildHugoSite}
89
+ * @param section - The section's slug
90
+ * @param siteUrl - The export's site URL, which permalinks start with
91
+ * @returns One slug per entry
92
+ * @example
93
+ * await readFeedSlugs(siteDir, "ideas", "https://example.com");
94
+ */
95
+ export async function readFeedSlugs(
96
+ siteDir: string,
97
+ section: string,
98
+ siteUrl: string,
99
+ ): Promise<string[]> {
100
+ const xml = await readFile(
101
+ join(siteDir, "public", section, "index.xml"),
102
+ "utf8",
103
+ );
104
+ const permalink = new RegExp(
105
+ `href="${siteUrl.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}/([^/"]+)/"`,
106
+ );
107
+ return [...xml.matchAll(/<entry>([\s\S]*?)<\/entry>/g)].map(
108
+ (entry) => entry[1]?.match(permalink)?.[1] ?? "",
109
+ );
110
+ }
111
+
112
+ /**
113
+ * The Thread roots a timeline page lists, page by page.
114
+ *
115
+ * A timeline renders each root as a thread preview or a lone post card, and
116
+ * both nest cards that carry their own `data-slug`, so this reads every
117
+ * `data-slug` in order and keeps the first mention of each root.
118
+ *
119
+ * @param siteDir - From {@link buildHugoSite}
120
+ * @param section - The section's slug, or `""` for the home page
121
+ * @param roots - Slugs of every Thread root in the export
122
+ * @returns One list of root slugs per page
123
+ * @example
124
+ * await readRootSlugsByPage(siteDir, "featured", new Set(["hello"]));
125
+ */
126
+ export async function readRootSlugsByPage(
127
+ siteDir: string,
128
+ section: string,
129
+ roots: ReadonlySet<string>,
130
+ ): Promise<string[][]> {
131
+ const pages: string[][] = [];
132
+ for (let page = 1; ; page++) {
133
+ const path =
134
+ page === 1
135
+ ? join(siteDir, "public", section, "index.html")
136
+ : join(siteDir, "public", section, "page", String(page), "index.html");
137
+ const html = await readFile(path, "utf8").catch(() => null);
138
+ if (html === null) return pages;
139
+ const slugs: string[] = [];
140
+ for (const match of html.matchAll(/data-slug="([^"]+)"/g)) {
141
+ const slug = match[1] as string;
142
+ if (roots.has(slug) && !slugs.includes(slug)) slugs.push(slug);
143
+ }
144
+ pages.push(slugs);
145
+ }
146
+ }