@jant/core 0.6.16 → 0.7.0

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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
package/dist/index.js CHANGED
@@ -1,6 +1,3 @@
1
- import { b as url_exports } from "./url-CbLAtlZe.js";
2
- import { C as toPostView, S as toNavItemViews, T as toSearchResultView, _ as createMediaContext, b as toMediaView, d as defaultFeedRenderer, t as createApp, v as toArchiveGroups, w as toPostViews, x as toNavItemView, y as toArchiveGroupsWithMedia } from "./app-C7_4oab9.js";
3
- import { B as NAV_ITEM_TYPES, E as time_exports, F as MAX_MEDIA_ATTACHMENTS, H as STATUSES, I as MAX_PINNED_POSTS, K as TEXT_ATTACHMENT_CONTENT_FORMATS, N as FORMATS, V as SORT_ORDERS, s as markdown_exports, z as MEDIA_KINDS } from "./export-Dl5KCiEs.js";
4
- import "./env-BPYViDJJ.js";
5
- import "./github-sync-HdK2EgvJ.js";
1
+ import { Fn as url_exports, Ht as STATUSES, Kt as TEXT_ATTACHMENT_CONTENT_FORMATS, Mt as MEDIA_KINDS, Ot as MAX_MEDIA_ATTACHMENTS, Pt as NAV_ITEM_TYPES, R as time_exports, Vt as SORT_ORDERS, kt as MAX_PINNED_POSTS, v as markdown_exports, wt as FORMATS } from "./github-sync-BPAvT999.js";
2
+ import { C as toNavItemViews, E as toSearchResultView, S as toNavItemView, T as toPostViews, _ as defaultFeedRenderer, b as toArchiveGroupsWithMedia, n as createApp, v as createMediaContext, w as toPostView, x as toMediaView, y as toArchiveGroups } from "./app-K_Aa1MMn.js";
6
3
  export { FORMATS, MAX_MEDIA_ATTACHMENTS, MAX_PINNED_POSTS, MEDIA_KINDS, NAV_ITEM_TYPES, SORT_ORDERS, STATUSES, TEXT_ATTACHMENT_CONTENT_FORMATS, createApp, createMediaContext, defaultFeedRenderer, markdown_exports as markdown, time_exports as time, toArchiveGroups, toArchiveGroupsWithMedia, toMediaView, toNavItemView, toNavItemViews, toPostView, toPostViews, toSearchResultView, url_exports as url };
package/dist/node.js CHANGED
@@ -1,8 +1,5 @@
1
- import "./url-CbLAtlZe.js";
2
- import { A as buildThemeStyle, D as getCjkFontCssVariables, E as BUILTIN_FONT_THEMES, M as getPublicAssetBasePath, N as isAssetPath, O as getFontThemeCssVariables, a as getHostBasedStartupConfigurationIssues, c as getWebhookUrl, f as createStorageDriver, g as schema_exports, h as createNodeDatabase, i as createSiteService, j as BUILTIN_COLOR_THEMES, k as resolveCjkFontProfile, l as setMyCommands, m as sqliteSchemaBundle, n as createNodeCliRuntime, o as resolveDatabaseDialect, p as pgSchemaBundle, r as createNodeRequestRuntime, s as resolveConfig, t as createApp, u as setWebhook } from "./app-C7_4oab9.js";
3
- import { t as createExportService } from "./export-Dl5KCiEs.js";
4
- import { C as shouldTrustProxy, S as getTelegramWebhookSecret, b as getSiteResolutionMode, d as getHostedControlPlaneBaseUrl, i as getConfiguredSingleSitePathPrefix, l as getEnvString, r as getConfiguredSingleSiteOrigin, x as getTelegramBotPool, y as getPort } from "./env-BPYViDJJ.js";
5
- import "./github-sync-HdK2EgvJ.js";
1
+ import { $ as getHostedControlPlaneBaseUrl, H as getConfiguredSingleSiteOrigin, U as getConfiguredSingleSitePathPrefix, Z as getEnvString, ct as getSiteResolutionMode, dt as shouldTrustProxy, lt as getTelegramBotPool, s as createExportService, st as getPort, ut as getTelegramWebhookSecret } from "./github-sync-BPAvT999.js";
2
+ import { A as resolveCjkFontProfile, D as BUILTIN_FONT_THEMES, M as BUILTIN_COLOR_THEMES, N as getPublicAssetBasePath, O as getCjkFontCssVariables, P as isAssetPath, a as createSiteService, c as resolveConfig, d as setWebhook, f as createStorageDriver, g as schema_exports, h as createNodeDatabase, i as createNodeRequestRuntime, j as buildThemeStyle, k as getFontThemeCssVariables, l as getWebhookUrl, m as sqliteSchemaBundle, n as createApp, o as getHostBasedStartupConfigurationIssues, p as pgSchemaBundle, r as createNodeCliRuntime, s as resolveDatabaseDialect, u as setMyCommands } from "./app-K_Aa1MMn.js";
6
3
  import { drizzle } from "drizzle-orm/better-sqlite3";
7
4
  import { serve } from "@hono/node-server";
8
5
  import Database from "better-sqlite3";
@@ -422,7 +419,8 @@ async function migrate(env = getProcessBindings()) {
422
419
  }
423
420
  const sqlite = createNodeSqlite(env, { createParentDir: true });
424
421
  try {
425
- migrate$1(drizzle(sqlite, { schema: schema_exports }), { migrationsFolder: resolveNodeMigrationsDir(import.meta.url, "sqlite") });
422
+ const db = drizzle(sqlite, { schema: schema_exports });
423
+ migrate$1(db, { migrationsFolder: resolveNodeMigrationsDir(import.meta.url, "sqlite") });
426
424
  } finally {
427
425
  sqlite.close();
428
426
  }
@@ -529,7 +527,7 @@ async function createNodeRequestHandler(options) {
529
527
  async function start(env = process.env, app) {
530
528
  const handler = await createNodeRequestHandler({
531
529
  env,
532
- app: async () => app ?? (await import("./app-C75Zc8dD.js")).createApp()
530
+ app: async () => app ?? (await import("./app-K_Aa1MMn.js").then((n) => n.t)).createApp()
533
531
  });
534
532
  const hostname = resolveHost(env);
535
533
  const port = resolvePort(env);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.6.16",
3
+ "version": "0.7.0",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "exports": {
@@ -48,7 +48,7 @@
48
48
  "@tiptap/starter-kit": "^3.27.3",
49
49
  "@tiptap/suggestion": "^3.27.3",
50
50
  "basecoat-css": "^0.3.11",
51
- "better-auth": "^1.5.5",
51
+ "better-auth": "1.6.27",
52
52
  "better-sqlite3": "^12.8.0",
53
53
  "blurhash": "^2.0.5",
54
54
  "drizzle-orm": "^0.45.1",
@@ -90,7 +90,7 @@
90
90
  "@lingui/conf": "^5.9.3",
91
91
  "@lingui/format-po": "^5.9.3",
92
92
  "@lingui/swc-plugin": "^5.11.0",
93
- "@swc/core": "^1.15.18",
93
+ "@swc/core": "1.15.47",
94
94
  "@tailwindcss/vite": "^4.2.2",
95
95
  "@types/better-sqlite3": "^7.6.13",
96
96
  "@types/node": "^25.5.0",
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { eq } from "drizzle-orm";
3
+ import { createAuth } from "../auth.js";
4
+ import { createTestDatabase } from "./helpers/db.js";
5
+ import { session as sessionTable } from "../db/schema.js";
6
+
7
+ /**
8
+ * better-auth gates `list-sessions` behind a "fresh session" check whose
9
+ * default window is 24 hours, measured from `session.createdAt` — a value
10
+ * renewal never moves. Jant deliberately runs 90-day rolling sessions, so
11
+ * without `freshAge: 0` the Sessions page answers "Session is not fresh"
12
+ * forever from the second day of a sign-in onwards.
13
+ */
14
+ async function signUpAndGetCookie(auth: ReturnType<typeof createAuth>) {
15
+ const signUp = await auth.api.signUpEmail({
16
+ body: {
17
+ name: "Owner",
18
+ email: "owner@example.com",
19
+ password: "correct-horse-battery",
20
+ },
21
+ returnHeaders: true,
22
+ });
23
+ const token = signUp.headers
24
+ .getSetCookie()
25
+ .find((cookie) => cookie.includes(".session_token="));
26
+ expect(token).toBeDefined();
27
+ // Send only the session token, not the `session_data` cache cookie: a cache
28
+ // hit short-circuits before the database read this test is about.
29
+ return token!.split(";")[0]!;
30
+ }
31
+
32
+ describe("session freshness", () => {
33
+ it("lists sessions that are older than better-auth's default fresh window", async () => {
34
+ const { db } = createTestDatabase();
35
+ const auth = createAuth(db, {
36
+ secret: "test-secret-with-enough-entropy-for-session-cookies",
37
+ baseURL: "https://example.com",
38
+ useSecureCookies: true,
39
+ });
40
+ const cookie = await signUpAndGetCookie(auth);
41
+
42
+ const [row] = await db.select().from(sessionTable);
43
+ expect(row).toBeDefined();
44
+ // Two days old: past the 24-hour default, well inside the 90-day window.
45
+ await db
46
+ .update(sessionTable)
47
+ .set({ createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000) })
48
+ .where(eq(sessionTable.id, row!.id));
49
+
50
+ const sessions = await auth.api.listSessions({
51
+ headers: new Headers({ cookie }),
52
+ });
53
+
54
+ expect(sessions).toHaveLength(1);
55
+ expect(sessions[0]!.token).toBe(row!.token);
56
+ });
57
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * BaseCoat variant classes are self-contained, and this is what says so out
3
+ * loud.
4
+ *
5
+ * AGENTS.md has carried the rule under Hard Constraints all along and it was
6
+ * broken anyway, more than once, because nothing failed when it was: the markup
7
+ * compiles, every other test passes, and the damage is visible only to an eye
8
+ * looking at the right pixel.
9
+ *
10
+ * What `class="btn btn-outline"` does, exactly: both are single-class
11
+ * selectors, so specificity ties and source order decides. `.btn-outline` is
12
+ * declared later, so its `bg-background` wins the background — but it sets no
13
+ * text colour, so `.btn`'s `text-primary-foreground` survives. A light label on
14
+ * a light fill: the button reads as empty until `:hover`, where
15
+ * `.btn-outline:hover` sets `text-accent-foreground` and the words appear. Each
16
+ * pairing fails for its own reason, which is why the rule is "never combine"
17
+ * rather than a list of bad pairs.
18
+ */
19
+
20
+ import { readdirSync, readFileSync, statSync } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { describe, expect, it } from "vitest";
24
+
25
+ const SRC_DIR = fileURLToPath(new URL("..", import.meta.url));
26
+
27
+ /**
28
+ * The BaseCoat families whose variants replace the base class rather than
29
+ * modify it. Anything of the form `<base>-<x>` is one of `<base>`'s variants.
30
+ */
31
+ const FAMILIES = ["btn", "badge", "alert"];
32
+
33
+ /** Class attributes as authored — JSX `className="…"` and Lit `class="…"` alike. */
34
+ const CLASS_ATTR = /\bclass(?:Name)?="([^"]*)"/g;
35
+
36
+ /**
37
+ * Walk the source tree for authored markup.
38
+ *
39
+ * @param dir - Directory to descend into
40
+ * @returns Absolute paths of every `.ts`/`.tsx` file outside tests
41
+ */
42
+ function collectSourceFiles(dir: string): string[] {
43
+ const found: string[] = [];
44
+ for (const entry of readdirSync(dir)) {
45
+ const path = join(dir, entry);
46
+ if (statSync(path).isDirectory()) {
47
+ if (entry === "__tests__" || entry === "node_modules") continue;
48
+ found.push(...collectSourceFiles(path));
49
+ } else if (entry.endsWith(".ts") || entry.endsWith(".tsx")) {
50
+ found.push(path);
51
+ }
52
+ }
53
+ return found;
54
+ }
55
+
56
+ /**
57
+ * Every class attribute in the repo's own markup, with the file it came from.
58
+ *
59
+ * A `class` built by interpolation is read for its literal words only. That is
60
+ * the point rather than a limitation: the pairing this guards against is
61
+ * written literally, and a regex should not try to evaluate a template.
62
+ *
63
+ * @returns One entry per class attribute found
64
+ */
65
+ function readClassAttributes(): Array<{ file: string; classes: string[] }> {
66
+ const found: Array<{ file: string; classes: string[] }> = [];
67
+
68
+ for (const file of collectSourceFiles(SRC_DIR)) {
69
+ const source = readFileSync(file, "utf8");
70
+ for (const match of source.matchAll(CLASS_ATTR)) {
71
+ found.push({
72
+ file: file.slice(SRC_DIR.length),
73
+ classes: (match[1] ?? "").split(/\s+/).filter(Boolean),
74
+ });
75
+ }
76
+ }
77
+
78
+ return found;
79
+ }
80
+
81
+ describe("BaseCoat variant classes", () => {
82
+ it("are never combined with their base class", () => {
83
+ const offenders: string[] = [];
84
+
85
+ for (const { file, classes } of readClassAttributes()) {
86
+ for (const base of FAMILIES) {
87
+ if (!classes.includes(base)) continue;
88
+ const variant = classes.find(
89
+ (name) => name !== base && name.startsWith(`${base}-`),
90
+ );
91
+ if (variant) {
92
+ offenders.push(`${file}: class="… ${base} ${variant} …"`);
93
+ }
94
+ }
95
+ }
96
+
97
+ expect(
98
+ offenders,
99
+ [
100
+ "A BaseCoat variant replaces the base class, it does not modify it.",
101
+ "Drop the bare base and keep the variant alone:",
102
+ ...offenders,
103
+ ].join("\n"),
104
+ ).toEqual([]);
105
+ });
106
+
107
+ // A guard is worth nothing if the shape it looks for is not the shape the
108
+ // markup writes, so this pins the reader against real files rather than
109
+ // trusting the regex.
110
+ it("reads class attributes out of the real markup", () => {
111
+ const attributes = readClassAttributes();
112
+
113
+ expect(attributes.length).toBeGreaterThan(100);
114
+ expect(attributes.some(({ classes }) => classes.includes("btn"))).toBe(
115
+ true,
116
+ );
117
+ expect(
118
+ attributes.some(({ classes }) => classes.includes("btn-outline")),
119
+ ).toBe(true);
120
+ });
121
+ });
@@ -0,0 +1,68 @@
1
+ import { brotliDecompressSync } from "node:zlib";
2
+ import { describe, expect, it } from "vitest";
3
+ import { compressForUpload } from "../../../bin/commands/assets/upload.js";
4
+
5
+ describe("compressForUpload", () => {
6
+ it("compresses text assets and round-trips them unchanged", () => {
7
+ // Repetitive like real CSS, so brotli has something to work with.
8
+ const body = Buffer.from(
9
+ ".a{color:var(--site-text-primary)}".repeat(200),
10
+ "utf8",
11
+ );
12
+
13
+ const packed = compressForUpload("client.css", body);
14
+
15
+ expect(packed).not.toBeNull();
16
+ expect(packed!.length).toBeLessThan(body.length);
17
+ expect(brotliDecompressSync(packed!).equals(body)).toBe(true);
18
+ });
19
+
20
+ it("leaves already-compressed types alone", () => {
21
+ // Running brotli over a woff2 or a png costs build time and usually grows
22
+ // the file; the CDN would then serve a needless encoding header too.
23
+ const body = Buffer.from("x".repeat(5000), "utf8");
24
+
25
+ for (const name of ["font.woff2", "logo.png", "icon.ico", "photo.jpg"]) {
26
+ expect(compressForUpload(name, body)).toBeNull();
27
+ }
28
+ });
29
+
30
+ it("falls back to the raw body when compression does not shrink it", () => {
31
+ // A few random bytes come back larger once the brotli header is added.
32
+ const body = Buffer.from([0x1f, 0x2e, 0x3d, 0x4c]);
33
+
34
+ expect(compressForUpload("tiny.json", body)).toBeNull();
35
+ });
36
+
37
+ it("matches on the extension case-insensitively", () => {
38
+ const body = Buffer.from("body{margin:0}".repeat(200), "utf8");
39
+
40
+ expect(compressForUpload("STYLE.CSS", body)).not.toBeNull();
41
+ });
42
+ });
43
+
44
+ describe("stale encoding detection", () => {
45
+ // `findStaleEncodingKeys` is not exported — it needs a live S3 client — so
46
+ // these pin the decision it is built on: which files we expect to find
47
+ // stored as brotli, and which correctly have no `Content-Encoding`.
48
+ it("expects compression for the types that carry the reader payload", () => {
49
+ const body = Buffer.from(".x{color:red}".repeat(400), "utf8");
50
+
51
+ // A CJK stylesheet is the case that motivated this: its key never changes,
52
+ // so it was skipped forever and kept being compressed at the CDN's own low
53
+ // quality on every request.
54
+ for (const name of ["client-cjk-B7Z0snDu.css", "client-CDPZMBLo.js"]) {
55
+ expect(compressForUpload(name, body)).not.toBeNull();
56
+ }
57
+ });
58
+
59
+ it("expects no compression where storing raw is the right answer", () => {
60
+ // Anything here would be re-uploaded on every run if a missing
61
+ // `Content-Encoding` counted as stale.
62
+ const dense = Buffer.from("y".repeat(4000), "utf8");
63
+ expect(compressForUpload("subset.woff2", dense)).toBeNull();
64
+
65
+ const tiny = Buffer.from([0x7a, 0x1b, 0x4f, 0x22]);
66
+ expect(compressForUpload("meta.json", tiny)).toBeNull();
67
+ });
68
+ });
@@ -196,6 +196,9 @@ describe("Hugo smoke build", () => {
196
196
  "public/hello-world/index.html",
197
197
  // Alias page for the reply slug (root aliases include /hello-reply/).
198
198
  "public/hello-reply/index.html",
199
+ // The feed redirects are useless unless Hugo copies them to the root of
200
+ // the published directory, where Cloudflare Pages and Netlify look.
201
+ "public/_redirects",
199
202
  ]) {
200
203
  expect(await fileExists(join(tempDir, rel)), `missing ${rel}`).toBe(true);
201
204
  }
@@ -218,6 +221,32 @@ describe("Hugo smoke build", () => {
218
221
  expect(collectionFeed).toContain("Follow-up");
219
222
  expect(collectionFeed.match(/<entry>/g)).toHaveLength(1);
220
223
 
224
+ // The exported feed carries the same entry payload the served one does,
225
+ // minus what Known Limits in docs/internal/feed-contract.md records. A
226
+ // template that stops compiling these silently drops them from every
227
+ // self-hosted export.
228
+ expect(collectionFeed).toContain('xmlns:jant="https://jant.me/ns"');
229
+ expect(collectionFeed).toContain(
230
+ 'xmlns:media="http://search.yahoo.com/mrss/"',
231
+ );
232
+ expect(collectionFeed).toContain("<jant:format>note</jant:format>");
233
+ // Nothing in this thread is cut, so the flag stays absent.
234
+ expect(collectionFeed).not.toContain("<jant:truncated/>");
235
+ expect(collectionFeed).toContain(
236
+ '<category term="ideas" label="Ideas" jant:page="https://example.com/ideas/"/>',
237
+ );
238
+ // Media RSS describes the attachment; the content shows it in the same
239
+ // `data-post-media` container the site marks its gallery strip with.
240
+ expect(collectionFeed).toContain('medium="image"');
241
+ expect(collectionFeed).toContain('width="800" height="600"');
242
+ expect(collectionFeed).toContain(
243
+ '<media:description type="plain">A photo</media:description>',
244
+ );
245
+ expect(collectionFeed).toContain("<div data-post-media>");
246
+ // An image is already shown full size inside a link to the original, so it
247
+ // gets no enclosure.
248
+ expect(collectionFeed).not.toContain('rel="enclosure"');
249
+
221
250
  const featuredHtml = await readFile(
222
251
  join(tempDir, "public/featured/index.html"),
223
252
  "utf-8",
@@ -234,4 +263,235 @@ describe("Hugo smoke build", () => {
234
263
  expect(featuredFeed).toContain("Follow-up");
235
264
  expect(featuredFeed.match(/<entry>/g)).toHaveLength(1);
236
265
  }, 60_000);
266
+
267
+ // Two feed details the served feed gets right and the template has to
268
+ // match by hand: author text inside an attribute, and the entry-level
269
+ // truncation flag on a thread.
270
+ it("escapes image alt in the feed and marks a thread truncated by a folded-in reply", async () => {
271
+ if (!hugoOk) {
272
+ console.log("hugo binary not found on PATH — skipping hugo build test");
273
+ return;
274
+ }
275
+
276
+ const para = (text: string) => ({
277
+ type: "paragraph",
278
+ content: [{ type: "text", text }],
279
+ });
280
+ const root = makePost({
281
+ id: "pst_blue",
282
+ slug: "blue-room",
283
+ title: "The blue room",
284
+ threadId: "pst_blue",
285
+ });
286
+ // The leading reply is cut by the timeline; the newest is not. The old
287
+ // rule looked only at the newest reply and would have left the entry
288
+ // unmarked. A titleless note is cut at NOTE_SUMMARY_MAX_CHARS with at
289
+ // least NOTE_SUMMARY_MIN_HIDDEN_CHARS left over, so four long paragraphs.
290
+ const longReply = makePost({
291
+ id: "pst_long",
292
+ slug: "blue-long",
293
+ title: null,
294
+ replyToId: "pst_blue",
295
+ threadId: "pst_blue",
296
+ createdAt: 1773020000,
297
+ publishedAt: 1773020000,
298
+ body: JSON.stringify({
299
+ type: "doc",
300
+ content: ["Alpha", "Beta", "Gamma", "Omega"].map((word) =>
301
+ para(`${word} `.repeat(90).trim()),
302
+ ),
303
+ }),
304
+ });
305
+ const shortReply = makePost({
306
+ id: "pst_short",
307
+ slug: "blue-short",
308
+ title: null,
309
+ replyToId: "pst_blue",
310
+ threadId: "pst_blue",
311
+ createdAt: 1773030000,
312
+ publishedAt: 1773030000,
313
+ body: JSON.stringify({ type: "doc", content: [para("Short enough.")] }),
314
+ });
315
+ // A quote and a backslash: Go's `%q` would turn both into `\"` and `\\`.
316
+ const alt = 'The "Blue" room, C:\\photos';
317
+ const media = makeMedia({ id: "med_blue", filename: "blue.webp", alt });
318
+
319
+ const services = {
320
+ posts: { list: async () => [root, longReply, shortReply] },
321
+ paths: {
322
+ getPostSlugMap: async () =>
323
+ new Map([
324
+ ["pst_blue", "blue-room"],
325
+ ["pst_long", "blue-long"],
326
+ ["pst_short", "blue-short"],
327
+ ]),
328
+ getPostAliases: async () => new Map(),
329
+ getCollectionSlugMap: async () => new Map(),
330
+ },
331
+ collections: {
332
+ list: async () => [],
333
+ listDirectoryData: async () => ({
334
+ collections: [],
335
+ items: [],
336
+ directoryItems: [],
337
+ }),
338
+ getCollectionsByPostIds: async () => new Map(),
339
+ getCollectionEntriesByThreadIds: async () => new Map(),
340
+ },
341
+ media: {
342
+ getByPostIds: async () => new Map([["pst_blue", [media]]]),
343
+ },
344
+ } as unknown as ServicesArg;
345
+
346
+ const siteDir = await mkdtemp(join(tmpdir(), "jant-hugo-feed-"));
347
+ try {
348
+ const files = await createExportService(
349
+ services,
350
+ makeSiteConfig(),
351
+ ).generateHugoFiles();
352
+ for (const file of files) {
353
+ const target = join(siteDir, file.path);
354
+ await mkdir(dirname(target), { recursive: true });
355
+ const data =
356
+ typeof file.content === "string"
357
+ ? new TextEncoder().encode(file.content)
358
+ : file.content;
359
+ await writeFile(target, data);
360
+ }
361
+ await writeFile(
362
+ join(siteDir, "content/blue-room/med_blue.webp"),
363
+ "PHOTO",
364
+ );
365
+
366
+ const { code, stdout, stderr } = await runHugo(siteDir);
367
+ if (code !== 0) {
368
+ console.error("hugo stdout:", stdout);
369
+ console.error("hugo stderr:", stderr);
370
+ }
371
+ expect(code).toBe(0);
372
+ expect(stderr).not.toMatch(/\bERROR\b/);
373
+
374
+ const feed = await readFile(join(siteDir, "public/index.xml"), "utf-8");
375
+ expect(feed.match(/<entry>/g)).toHaveLength(1);
376
+
377
+ // The attribute is XML-escaped like the caption next to it, not
378
+ // Go-quoted: `"` becomes a character reference and `\` stays itself.
379
+ const escapedAlt = "The &#34;Blue&#34; room, C:\\photos";
380
+ expect(feed).toContain(`alt="${escapedAlt}"`);
381
+ expect(feed).toContain(`<figcaption>${escapedAlt}</figcaption>`);
382
+ expect(feed).not.toContain('alt="The \\"');
383
+
384
+ expect(feed).toContain("<jant:truncated/>");
385
+ } finally {
386
+ await rm(siteDir, { recursive: true, force: true });
387
+ }
388
+ }, 60_000);
389
+
390
+ it("folds a long thread's preview the way the site does", async () => {
391
+ if (!hugoOk) {
392
+ console.log("hugo binary not found on PATH — skipping hugo build test");
393
+ return;
394
+ }
395
+
396
+ const words = ["one", "two", "three", "four", "five", "six", "seven"];
397
+ const root = makePost({
398
+ id: "pst_fold",
399
+ slug: "fold-root",
400
+ title: "A long thread",
401
+ threadId: "pst_fold",
402
+ });
403
+ const replies = words.map((word, index) =>
404
+ makePost({
405
+ id: `pst_fold_${word}`,
406
+ slug: `fold-${word}`,
407
+ title: null,
408
+ replyToId: "pst_fold",
409
+ threadId: "pst_fold",
410
+ createdAt: 1773020000 + index * 1000,
411
+ publishedAt: 1773020000 + index * 1000,
412
+ body: JSON.stringify({
413
+ type: "doc",
414
+ content: [
415
+ {
416
+ type: "paragraph",
417
+ content: [{ type: "text", text: `Reply ${word} of the fold.` }],
418
+ },
419
+ ],
420
+ }),
421
+ }),
422
+ );
423
+
424
+ const services = {
425
+ posts: { list: async () => [root, ...replies] },
426
+ paths: {
427
+ getPostSlugMap: async () =>
428
+ new Map([
429
+ ["pst_fold", "fold-root"],
430
+ ...words.map((word): [string, string] => [
431
+ `pst_fold_${word}`,
432
+ `fold-${word}`,
433
+ ]),
434
+ ]),
435
+ getPostAliases: async () => new Map(),
436
+ getCollectionSlugMap: async () => new Map(),
437
+ },
438
+ collections: {
439
+ list: async () => [],
440
+ listDirectoryData: async () => ({
441
+ collections: [],
442
+ items: [],
443
+ directoryItems: [],
444
+ }),
445
+ getCollectionsByPostIds: async () => new Map(),
446
+ getCollectionEntriesByThreadIds: async () => new Map(),
447
+ },
448
+ media: { getByPostIds: async () => new Map() },
449
+ } as unknown as ServicesArg;
450
+
451
+ const siteDir = await mkdtemp(join(tmpdir(), "jant-hugo-fold-"));
452
+ try {
453
+ const files = await createExportService(
454
+ services,
455
+ makeSiteConfig(),
456
+ ).generateHugoFiles();
457
+ for (const file of files) {
458
+ const target = join(siteDir, file.path);
459
+ await mkdir(dirname(target), { recursive: true });
460
+ const data =
461
+ typeof file.content === "string"
462
+ ? new TextEncoder().encode(file.content)
463
+ : file.content;
464
+ await writeFile(target, data);
465
+ }
466
+
467
+ const { code, stdout, stderr } = await runHugo(siteDir);
468
+ if (code !== 0) {
469
+ console.error("hugo stdout:", stdout);
470
+ console.error("hugo stderr:", stderr);
471
+ }
472
+ expect(code).toBe(0);
473
+
474
+ const html = await readFile(join(siteDir, "public/index.html"), "utf-8");
475
+ // Core's fold: the first two replies, a gap, the two before the newest,
476
+ // and the newest as the hero. Seven replies hide two.
477
+ for (const shown of ["one", "two", "five", "six", "seven"]) {
478
+ expect(html).toContain(`Reply ${shown} of the fold.`);
479
+ }
480
+ for (const hidden of ["three", "four"]) {
481
+ expect(html).not.toContain(`Reply ${hidden} of the fold.`);
482
+ }
483
+ expect(html).toContain("2 more posts");
484
+ // The gap leads to the first hidden reply, as the site's does. The
485
+ // build is minified, so attribute quotes are not guaranteed.
486
+ expect(html).toMatch(/thread-preview-gap"? href="?[^" >]*\/fold-three\//);
487
+ // A reply's card links to the reply's own address — its alias on the
488
+ // root — not to an empty href, since reply pages are never rendered.
489
+ expect(html).toMatch(
490
+ /post-card-permalink"? href="?[^" >]*\/fold-seven\//,
491
+ );
492
+ expect(html).not.toMatch(/post-card-permalink"? href(?=[ >])/);
493
+ } finally {
494
+ await rm(siteDir, { recursive: true, force: true });
495
+ }
496
+ }, 60_000);
237
497
  });