@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
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Tests for what the first-run screens say.
3
+ *
4
+ * These render the component rather than the route: the point under test is
5
+ * that an author is told which step this is — a hosted one landing on an
6
+ * unfamiliar domain, whose site it belongs to; a self-hosted one, how much of
7
+ * setup is left. Neither is something the POST handler or the onboarding
8
+ * middleware can vouch for.
9
+ */
10
+
11
+ import type { Context } from "hono";
12
+ import { renderToString } from "hono/jsx/dom/server";
13
+ import { describe, expect, it } from "vitest";
14
+ import { I18nProvider } from "../../../i18n/context.js";
15
+ import { createI18n } from "../../../i18n/i18n.js";
16
+ import type { DiscoverPageUrls } from "../../../lib/discover.js";
17
+ import { getDiscoverCopy } from "../../../ui/dash/settings/discover-copy.js";
18
+ import { SetupContent } from "../setup.js";
19
+
20
+ type SetupProps = Parameters<typeof SetupContent>[0];
21
+
22
+ const PAGES: DiscoverPageUrls = {
23
+ home: "https://jant.me/discover",
24
+ links: "https://jant.me/links",
25
+ quotes: "https://jant.me/quotes",
26
+ rules: "https://jant.me/discover/about",
27
+ };
28
+
29
+ function render(props: SetupProps): string {
30
+ const i18n = createI18n("en");
31
+ const c = {
32
+ get: (key: string) => (key === "i18n" ? i18n : undefined),
33
+ } as unknown as Context;
34
+
35
+ I18nProvider({ c, children: "" });
36
+ return renderToString(SetupContent(props));
37
+ }
38
+
39
+ describe("SetupContent — hosted site", () => {
40
+ const provisioned: SetupProps = {
41
+ mode: "site",
42
+ askSiteName: false,
43
+ contentLanguage: "en",
44
+ siteName: "My Blog",
45
+ discoverAvailable: true,
46
+ discoverDefault: true,
47
+ discoverPages: PAGES,
48
+ };
49
+
50
+ it("names the step and the site in one line", () => {
51
+ const html = render(provisioned);
52
+
53
+ expect(html).toContain("Setup · My Blog");
54
+ expect(html).toContain("What language do you write in?");
55
+ });
56
+
57
+ it("keeps the line when the site has no name", () => {
58
+ const { siteName: _omitted, ...unnamed } = provisioned;
59
+ const html = render(unnamed);
60
+
61
+ expect(html).toContain(">Setup</p>");
62
+ expect(html).not.toContain("·");
63
+ });
64
+
65
+ it("drops a blank name rather than trailing a separator", () => {
66
+ const html = render({ ...provisioned, siteName: " " });
67
+
68
+ expect(html).not.toContain("·");
69
+ });
70
+
71
+ it("asks nothing but the language and Discover", () => {
72
+ const html = render(provisioned);
73
+
74
+ expect(html).toContain("setup-content-language");
75
+ expect(html).not.toContain("setup-email");
76
+ expect(html).not.toContain("setup-password");
77
+ expect(html).not.toContain("setup-site-name");
78
+ });
79
+
80
+ // A hosted author never saw a first screen — they created their account in
81
+ // the control plane — so counting steps at them would name one they cannot
82
+ // account for.
83
+ it("counts no steps on the only screen it shows", () => {
84
+ const html = render(provisioned);
85
+
86
+ expect(html).not.toContain("Step");
87
+ });
88
+
89
+ // The control plane already set the clock; this screen can be opened from
90
+ // anywhere, and the browser reporting another zone is not a decision.
91
+ it("reports no time zone", () => {
92
+ const html = render(provisioned);
93
+
94
+ expect(html).not.toContain("timezone");
95
+ });
96
+ });
97
+
98
+ /**
99
+ * The box's state is carried by the Datastar signal, not by a `checked`
100
+ * attribute: `data-bind` initialises the control from the signal on upgrade, so
101
+ * the signal is what the server actually decides.
102
+ */
103
+ describe("SetupContent — the Discover question", () => {
104
+ const base: SetupProps = {
105
+ mode: "site",
106
+ askSiteName: true,
107
+ contentLanguage: "en",
108
+ discoverAvailable: true,
109
+ discoverDefault: false,
110
+ discoverPages: PAGES,
111
+ };
112
+
113
+ it("starts clear where the deployment lists nothing by default", () => {
114
+ const html = render(base);
115
+
116
+ expect(html).toContain("setup-discover");
117
+ expect(html).toContain("discover: false");
118
+ });
119
+
120
+ // Hosted Jant sets `DISCOVER=latest`, and the box has to say so rather than
121
+ // showing a refusal the site would not honour.
122
+ it("starts ticked where the deployment lists its blogs", () => {
123
+ const html = render({ ...base, discoverDefault: true });
124
+
125
+ expect(html).toContain("discover: true");
126
+ });
127
+
128
+ it("is asked on the hosted screen too", () => {
129
+ const html = render({
130
+ ...base,
131
+ askSiteName: false,
132
+ siteName: "My Blog",
133
+ discoverDefault: true,
134
+ });
135
+
136
+ expect(html).toContain("setup-discover");
137
+ expect(html).toContain("discover: true");
138
+ });
139
+
140
+ // Demo mode and feeds-off both outlive setup, so the question would be a
141
+ // promise the next screen breaks.
142
+ it("is not asked where the answer could not be honoured", () => {
143
+ const html = render({
144
+ ...base,
145
+ discoverAvailable: false,
146
+ discoverDefault: true,
147
+ });
148
+
149
+ expect(html).not.toContain("setup-discover");
150
+ expect(html).toContain("discover: false");
151
+ });
152
+
153
+ // One control appearing twice should not describe itself two ways: the
154
+ // label is the settings page's, and the help line is the settings page's
155
+ // opening sentence rather than a wording of setup's own. The delay, the
156
+ // lists and the rules wait for Settings, where an author has posts to show.
157
+ it("opens the way the settings page does, and stops there", () => {
158
+ const copy = getDiscoverCopy(createI18n("en"));
159
+ const html = render({ ...base, discoverPages: null });
160
+
161
+ expect(copy.intro.startsWith(copy.about)).toBe(true);
162
+ expect(html).toContain(`<span>${copy.label}</span>`);
163
+ expect(html).toContain(`${copy.about}</p>`);
164
+ expect(html).not.toContain("Featured");
165
+ expect(html).not.toContain(copy.rules);
166
+ });
167
+
168
+ // What Discover is, is best answered by the list itself, so its name in the
169
+ // help line is the way there — the same link the settings page puts on it.
170
+ it("links the directory's name in the help line to the directory", () => {
171
+ const html = render(base);
172
+
173
+ expect(html).toContain(
174
+ `<a href="${PAGES.home}" target="_blank" rel="noopener noreferrer" class="underline hover:text-foreground transition-colors">Jant Discover</a> is a directory of Jant blogs`,
175
+ );
176
+ expect(html.match(/<a href/g)).toHaveLength(1);
177
+ });
178
+
179
+ // A self-hosted site that announces to no directory has no address to link,
180
+ // and a sentence with a dead link in it is worse than a plain one.
181
+ it("leaves the help line plain when no directory is configured", () => {
182
+ const html = render({ ...base, discoverPages: null });
183
+
184
+ expect(html).toContain("Jant Discover is a directory of Jant blogs");
185
+ expect(html).not.toContain("<a href");
186
+ });
187
+
188
+ // The screen's footnote already says every answer here can change later.
189
+ // Saying it again under one checkbox was saying it twice.
190
+ it("does not repeat the way back to Settings", () => {
191
+ const copy = getDiscoverCopy(createI18n("en"));
192
+ const html = render({ ...base, discoverPages: null });
193
+
194
+ expect(html).not.toContain("→");
195
+ expect(html).toContain(`${copy.about}</p>`);
196
+ });
197
+ });
198
+
199
+ /**
200
+ * Where "you can change this later" sits. Under the heading, before the
201
+ * questions, it reads as permission to leave them blank; after the button it is
202
+ * only what it is.
203
+ */
204
+ describe("SetupContent — the note that nothing is final", () => {
205
+ const site: SetupProps = {
206
+ mode: "site",
207
+ askSiteName: true,
208
+ contentLanguage: "en",
209
+ discoverAvailable: true,
210
+ discoverDefault: false,
211
+ discoverPages: null,
212
+ };
213
+ const note = "You can change all of this later in Settings.";
214
+
215
+ it("sits in the card's footer, after the button", () => {
216
+ const html = render(site);
217
+
218
+ expect(html).toContain(
219
+ `<footer><p class="text-sm text-muted-foreground">${note}</p></footer>`,
220
+ );
221
+ expect(html.indexOf(note)).toBeGreaterThan(html.indexOf("</button>"));
222
+ });
223
+
224
+ it("leaves nothing under the heading", () => {
225
+ const html = render(site);
226
+ const header = html.slice(
227
+ html.indexOf("<header>"),
228
+ html.indexOf("</header>"),
229
+ );
230
+
231
+ expect(header).toContain("<h2>");
232
+ expect(header).not.toContain(note);
233
+ expect(header.match(/<p/g)).toHaveLength(1);
234
+ });
235
+
236
+ // Same component, same reason: the hosted variant asks a question in its
237
+ // heading, and a reassurance sitting right under it dilutes the question.
238
+ it("sits in the same place on the hosted screen", () => {
239
+ const html = render({ ...site, askSiteName: false, siteName: "My Blog" });
240
+ const header = html.slice(
241
+ html.indexOf("<header>"),
242
+ html.indexOf("</header>"),
243
+ );
244
+
245
+ expect(header).not.toContain(note);
246
+ expect(html.indexOf(note)).toBeGreaterThan(html.indexOf("</button>"));
247
+ });
248
+
249
+ // The account screen has no answers to reassure anyone about.
250
+ it("is absent from the account screen", () => {
251
+ const html = render({ mode: "account" });
252
+
253
+ expect(html).not.toContain("<footer>");
254
+ });
255
+ });
256
+
257
+ describe("SetupContent — self-hosted, first screen", () => {
258
+ const account: SetupProps = { mode: "account" };
259
+
260
+ it("wears the same one-line shell as the hosted screen, plus the count", () => {
261
+ const html = render(account);
262
+
263
+ expect(html).toContain("Setup · Step 1 of 2");
264
+ expect(html).toContain("Welcome to Jant");
265
+ });
266
+
267
+ // The whole reason credentials get a screen of their own: an email and a
268
+ // password with nothing between them is the shape password managers look for.
269
+ it("asks for credentials and nothing else", () => {
270
+ const html = render(account);
271
+
272
+ expect(html).toContain("setup-email");
273
+ expect(html).toContain("setup-password");
274
+ expect(html).not.toContain("setup-site-name");
275
+ expect(html).not.toContain("setup-content-language");
276
+ expect(html).not.toContain("setup-discover");
277
+ });
278
+
279
+ // "Complete Setup" here would make the next screen read as a rejection.
280
+ it("says it continues rather than finishes", () => {
281
+ const html = render(account);
282
+
283
+ expect(html).toContain(">Continue</button>");
284
+ expect(html).not.toContain("Complete Setup");
285
+ });
286
+ });
287
+
288
+ describe("SetupContent — self-hosted, second screen", () => {
289
+ const site: SetupProps = {
290
+ mode: "site",
291
+ askSiteName: true,
292
+ contentLanguage: "en",
293
+ discoverAvailable: true,
294
+ discoverDefault: false,
295
+ discoverPages: PAGES,
296
+ };
297
+
298
+ it("counts itself as the last step and names no site yet", () => {
299
+ const html = render(site);
300
+
301
+ expect(html).toContain("Setup · Step 2 of 2");
302
+ expect(html).toContain("Set up your site");
303
+ });
304
+
305
+ it("asks for the site and not the account", () => {
306
+ const html = render(site);
307
+
308
+ expect(html).toContain("setup-site-name");
309
+ expect(html).toContain("setup-content-language");
310
+ expect(html).toContain("setup-discover");
311
+ expect(html).not.toContain("setup-email");
312
+ expect(html).not.toContain("setup-password");
313
+ });
314
+
315
+ // The install that is still choosing its own clock is the one that reports
316
+ // it, so the site's time zone is settled by the screen that names the site.
317
+ it("reports the browser's time zone", () => {
318
+ const html = render(site);
319
+
320
+ expect(html).toContain("timezone");
321
+ });
322
+
323
+ it("ends the flow rather than continuing it", () => {
324
+ const html = render(site);
325
+
326
+ expect(html).toContain("Start writing");
327
+ expect(html).not.toContain(">Continue</button>");
328
+ });
329
+ });
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Tests for which of the two self-hosted setup screens a request gets.
3
+ *
4
+ * The step is never in the URL. It is read from the onboarding status and, on
5
+ * the second screen, from whether the site has a name yet — the only records
6
+ * that survive a closed tab, a reload, or a back button. These cover the route
7
+ * making that choice; `setup-page.test.tsx` covers what each screen then says.
8
+ */
9
+
10
+ import { describe, expect, it, vi } from "vitest";
11
+ import { Hono } from "hono";
12
+ import { createI18n } from "../../../i18n/index.js";
13
+ import { attachSession } from "../../../middleware/session.js";
14
+ import { ONBOARDING_STATUS } from "../../../lib/constants.js";
15
+ import { setupRoutes } from "../setup.js";
16
+ import type { Bindings } from "../../../types.js";
17
+ import type { AppVariables } from "../../../types/app-context.js";
18
+
19
+ type Env = { Bindings: Bindings; Variables: AppVariables };
20
+
21
+ const SESSION_COOKIE = "jant.session_token=abc; Path=/; HttpOnly";
22
+
23
+ function createMockAuth(signedIn: boolean) {
24
+ const signUpEmail = vi.fn(async () => ({
25
+ headers: new Headers({ "Set-Cookie": SESSION_COOKIE }),
26
+ response: { user: { id: "usr_new-owner" } },
27
+ }));
28
+ const updateUser = vi.fn(async () => ({}));
29
+
30
+ return {
31
+ signUpEmail,
32
+ updateUser,
33
+ auth: {
34
+ api: {
35
+ signUpEmail,
36
+ updateUser,
37
+ getSession: async () => ({
38
+ headers: new Headers(),
39
+ response: signedIn
40
+ ? {
41
+ user: { id: "usr_owner", email: "owner@test.com", name: "O" },
42
+ session: { id: "ses_owner" },
43
+ }
44
+ : null,
45
+ }),
46
+ },
47
+ } as unknown as AppVariables["auth"],
48
+ };
49
+ }
50
+
51
+ function createSetupApp(options: {
52
+ status: (typeof ONBOARDING_STATUS)[keyof typeof ONBOARDING_STATUS];
53
+ /** What `SITE_NAME` holds — null on a site nobody has named yet. */
54
+ storedSiteName?: string | null;
55
+ signedIn?: boolean;
56
+ isMember?: boolean;
57
+ }) {
58
+ const provisionOwnerAccount = vi.fn(async () => {});
59
+ const completeSiteSetup = vi.fn(async () => {});
60
+ const updateDiscoverSetting = vi.fn(async () => ({ shouldAnnounce: false }));
61
+ const { auth, signUpEmail, updateUser } = createMockAuth(
62
+ options.signedIn ?? false,
63
+ );
64
+
65
+ const app = new Hono<Env>();
66
+ app.use("*", async (c, next) => {
67
+ c.env = {} as Bindings;
68
+ c.set("auth", auth);
69
+ c.set("services", {
70
+ settings: {
71
+ getOnboardingStatus: async () => options.status,
72
+ get: async (key: string) =>
73
+ key === "SITE_NAME" ? (options.storedSiteName ?? null) : null,
74
+ updateDiscoverSetting,
75
+ },
76
+ bootstrap: { provisionOwnerAccount, completeSiteSetup },
77
+ siteMembers: {
78
+ get: async () =>
79
+ options.isMember
80
+ ? {
81
+ createdAt: 0,
82
+ role: "owner",
83
+ siteId: "sit_test",
84
+ updatedAt: 0,
85
+ userId: "usr_owner",
86
+ }
87
+ : null,
88
+ },
89
+ } as unknown as AppVariables["services"]);
90
+ c.set("currentSite", {
91
+ createdAt: 0,
92
+ id: "sit_test",
93
+ key: "default",
94
+ status: "active",
95
+ updatedAt: 0,
96
+ });
97
+ c.set("appConfig", {
98
+ siteName: "Jant",
99
+ sitePathPrefix: "",
100
+ siteLanguage: "en",
101
+ demoMode: false,
102
+ noindex: false,
103
+ rssFeedsEnabled: true,
104
+ } as AppVariables["appConfig"]);
105
+ c.set("lang", "en");
106
+ c.set("i18n", createI18n("en"));
107
+ await next();
108
+ });
109
+ app.use("*", attachSession());
110
+ app.route("/", setupRoutes);
111
+
112
+ return {
113
+ app,
114
+ provisionOwnerAccount,
115
+ completeSiteSetup,
116
+ signUpEmail,
117
+ updateUser,
118
+ };
119
+ }
120
+
121
+ function post(app: Hono<Env>, body: Record<string, unknown>) {
122
+ return app.request("/setup", {
123
+ method: "POST",
124
+ headers: { "Content-Type": "application/json" },
125
+ body: JSON.stringify(body),
126
+ });
127
+ }
128
+
129
+ describe("GET /setup picks the step", () => {
130
+ it("asks an empty site for credentials, with no session needed", async () => {
131
+ const { app } = createSetupApp({ status: ONBOARDING_STATUS.PENDING });
132
+
133
+ const html = await (await app.request("/setup")).text();
134
+
135
+ expect(html).toContain("Setup · Step 1 of 2");
136
+ expect(html).toContain("setup-password");
137
+ expect(html).not.toContain("setup-site-name");
138
+ });
139
+
140
+ // The state the site rests in between the screens: an owner, and no answers.
141
+ it("asks a provisioned but unnamed site for its name", async () => {
142
+ const { app } = createSetupApp({
143
+ status: ONBOARDING_STATUS.PROVISIONED,
144
+ storedSiteName: null,
145
+ signedIn: true,
146
+ isMember: true,
147
+ });
148
+
149
+ const html = await (await app.request("/setup")).text();
150
+
151
+ expect(html).toContain("Setup · Step 2 of 2");
152
+ expect(html).toContain("setup-site-name");
153
+ expect(html).not.toContain("setup-password");
154
+ });
155
+
156
+ // The hosted arrival at the same screen. A name it already has is a question
157
+ // it must not be asked again.
158
+ it("asks a named site only what it is missing", async () => {
159
+ const { app } = createSetupApp({
160
+ status: ONBOARDING_STATUS.PROVISIONED,
161
+ storedSiteName: "My Blog",
162
+ signedIn: true,
163
+ isMember: true,
164
+ });
165
+
166
+ const html = await (await app.request("/setup")).text();
167
+
168
+ expect(html).not.toContain("setup-site-name");
169
+ expect(html).not.toContain("Step");
170
+ expect(html).toContain("setup-content-language");
171
+ });
172
+
173
+ it("sends a finished site home", async () => {
174
+ const { app } = createSetupApp({ status: ONBOARDING_STATUS.COMPLETED });
175
+
176
+ const res = await app.request("/setup", { redirect: "manual" });
177
+
178
+ expect(res.status).toBe(302);
179
+ expect(res.headers.get("Location")).toBe("/");
180
+ });
181
+ });
182
+
183
+ describe("POST /setup, the account step", () => {
184
+ it("opens the account, stands the site up, and comes back for step two", async () => {
185
+ const { app, provisionOwnerAccount, signUpEmail } = createSetupApp({
186
+ status: ONBOARDING_STATUS.PENDING,
187
+ });
188
+
189
+ const res = await post(app, {
190
+ email: " Owner@Example.COM ",
191
+ password: "correct-horse-battery",
192
+ });
193
+
194
+ // Named after the address until the next screen has a site name to put
195
+ // there, and normalized before it is used for either.
196
+ expect(signUpEmail).toHaveBeenCalledWith(
197
+ expect.objectContaining({
198
+ body: {
199
+ name: "owner",
200
+ email: "owner@example.com",
201
+ password: "correct-horse-battery",
202
+ },
203
+ }),
204
+ );
205
+ expect(provisionOwnerAccount).toHaveBeenCalledWith({
206
+ ownerUserId: "usr_new-owner",
207
+ });
208
+ await expect(res.text()).resolves.toContain("/setup");
209
+ });
210
+
211
+ // The whole reason there is no `/signin` hop between the screens: the second
212
+ // one can only be answered by a member, so the first has to leave a session
213
+ // behind.
214
+ it("signs the new owner in on the way out", async () => {
215
+ const { app } = createSetupApp({ status: ONBOARDING_STATUS.PENDING });
216
+
217
+ const res = await post(app, {
218
+ email: "owner@example.com",
219
+ password: "correct-horse-battery",
220
+ });
221
+
222
+ expect(res.headers.get("Set-Cookie")).toBe(SESSION_COOKIE);
223
+ });
224
+
225
+ it("rejects a password too short to be one", async () => {
226
+ const { app, provisionOwnerAccount } = createSetupApp({
227
+ status: ONBOARDING_STATUS.PENDING,
228
+ });
229
+
230
+ const res = await post(app, { email: "owner@example.com", password: "sh" });
231
+
232
+ await expect(res.text()).resolves.toContain("8 characters");
233
+ expect(provisionOwnerAccount).not.toHaveBeenCalled();
234
+ });
235
+ });
236
+
237
+ describe("POST /setup, the site step", () => {
238
+ const owner = {
239
+ status: ONBOARDING_STATUS.PROVISIONED,
240
+ storedSiteName: null,
241
+ signedIn: true,
242
+ isMember: true,
243
+ } as const;
244
+
245
+ it("carries the name, the language, and the browser's clock", async () => {
246
+ const { app, completeSiteSetup } = createSetupApp(owner);
247
+
248
+ await post(app, {
249
+ siteName: "My Blog",
250
+ contentLanguage: "zh-Hans",
251
+ language: "en-GB",
252
+ timezone: "Asia/Shanghai",
253
+ discover: true,
254
+ });
255
+
256
+ expect(completeSiteSetup).toHaveBeenCalledWith(
257
+ {
258
+ siteName: "My Blog",
259
+ siteLanguage: "zh-Hans",
260
+ browserLanguage: "en-GB",
261
+ timeZone: "Asia/Shanghai",
262
+ },
263
+ { oldLanguage: "en" },
264
+ expect.anything(),
265
+ );
266
+ });
267
+
268
+ // The rename better-auth needs, wired through the service rather than run in
269
+ // the route, and reaching better-auth with the request's own headers.
270
+ it("hands the service a way to rename the operator's account", async () => {
271
+ const { app, completeSiteSetup, updateUser } = createSetupApp(owner);
272
+
273
+ await post(app, { siteName: "My Blog", contentLanguage: "en" });
274
+
275
+ const deps = completeSiteSetup.mock.calls[0]?.[2] as {
276
+ updateCurrentUserName: (name: string) => Promise<void>;
277
+ };
278
+ await deps.updateCurrentUserName("My Blog");
279
+
280
+ expect(updateUser).toHaveBeenCalledWith(
281
+ expect.objectContaining({ body: { name: "My Blog" } }),
282
+ );
283
+ });
284
+
285
+ it("refuses to finish an unnamed site with no name", async () => {
286
+ const { app, completeSiteSetup } = createSetupApp(owner);
287
+
288
+ const res = await post(app, { contentLanguage: "en" });
289
+
290
+ await expect(res.text()).resolves.toContain("Site name");
291
+ expect(completeSiteSetup).not.toHaveBeenCalled();
292
+ });
293
+
294
+ // A named site is never asked for one, so a name arriving anyway is not the
295
+ // author renaming their site from the setup screen.
296
+ it("leaves a named site's name alone", async () => {
297
+ const { app, completeSiteSetup } = createSetupApp({
298
+ ...owner,
299
+ storedSiteName: "My Blog",
300
+ });
301
+
302
+ await post(app, { siteName: "Something Else", contentLanguage: "en" });
303
+
304
+ expect(completeSiteSetup).toHaveBeenCalledWith(
305
+ expect.objectContaining({ siteName: undefined, timeZone: undefined }),
306
+ expect.anything(),
307
+ expect.anything(),
308
+ );
309
+ });
310
+ });