@jant/core 0.6.15 → 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 (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -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-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  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-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  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__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -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
+ });
@@ -1,26 +1,72 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { createTestDatabase } from "../../../__tests__/helpers/db.js";
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import {
4
+ createTestDatabase,
5
+ DEFAULT_TEST_SITE_ID,
6
+ } from "../../../__tests__/helpers/db.js";
3
7
  import { sql } from "drizzle-orm";
4
- import { navItems, settings, siteDomains, sites } from "../../../db/schema.js";
8
+ import {
9
+ navItems,
10
+ settings,
11
+ siteDomains,
12
+ siteMembers,
13
+ sites,
14
+ } from "../../../db/schema.js";
5
15
  import { createBootstrapService } from "../../../services/bootstrap.js";
16
+ import {
17
+ createSiteService,
18
+ TRANSIENT_SINGLE_SITE_ID,
19
+ } from "../../../services/site.js";
6
20
  import type { Database } from "../../../db/index.js";
7
21
  import type { BootstrapService } from "../../../services/bootstrap.js";
8
22
 
9
23
  /**
10
- * Reproduces the shell bootstrap logic from POST /setup to verify
11
- * setup stays idempotent even when managed shell data already exists.
24
+ * The bootstrap service one self-hosted setup request gets: bound to the site
25
+ * that request resolved, the way the runtime binds it. Resolved afresh for
26
+ * each screen, because the account step is what creates the site the second
27
+ * screen finds.
28
+ */
29
+ async function bootstrapForRequest(db: Database): Promise<BootstrapService> {
30
+ const { site } = await createSiteService(db).resolveSingleSite();
31
+ return createBootstrapService(db, site.id);
32
+ }
33
+
34
+ /**
35
+ * Reproduces both halves of POST /setup back to back — the shape a self-hosted
36
+ * install ends up in — to verify setup stays idempotent even when managed shell
37
+ * data already exists.
12
38
  */
13
39
  async function runSetupBootstrap(
14
- services: { bootstrap: BootstrapService },
15
- overrides: Partial<
16
- Parameters<BootstrapService["completeInitialSetup"]>[0]
17
- > = {},
40
+ services: { db: Database },
41
+ overrides: Partial<Parameters<BootstrapService["completeSiteSetup"]>[0]> = {},
18
42
  ) {
19
- await services.bootstrap.completeInitialSetup({
43
+ await (
44
+ await bootstrapForRequest(services.db)
45
+ ).provisionOwnerAccount({
20
46
  ownerUserId: "usr_test-owner",
21
- siteName: "Jant Demo",
22
- ...overrides,
23
47
  });
48
+ await (
49
+ await bootstrapForRequest(services.db)
50
+ ).completeSiteSetup(
51
+ { siteName: "Jant Demo", ...overrides },
52
+ { oldLanguage: "" },
53
+ );
54
+ }
55
+
56
+ /** Adds a second tenant, which is what a hosted database always has. */
57
+ async function insertOtherSite(
58
+ db: Database,
59
+ id = "sit_other00000000000000000000",
60
+ ) {
61
+ const timestamp = Math.floor(Date.now() / 1000);
62
+ await db.insert(sites).values({
63
+ id,
64
+ key: "other",
65
+ status: "active",
66
+ createdAt: timestamp - 60,
67
+ updatedAt: timestamp - 60,
68
+ });
69
+ return id;
24
70
  }
25
71
 
26
72
  describe("Setup bootstrap logic", () => {
@@ -34,7 +80,8 @@ describe("Setup bootstrap logic", () => {
34
80
  const db = testDb.db as unknown as Database;
35
81
  services = {
36
82
  db,
37
- bootstrap: createBootstrapService(db),
83
+ // The site both screens resolve once its row exists.
84
+ bootstrap: createBootstrapService(db, DEFAULT_TEST_SITE_ID),
38
85
  };
39
86
  });
40
87
 
@@ -46,9 +93,9 @@ describe("Setup bootstrap logic", () => {
46
93
 
47
94
  expect(navItemsList.map((item) => item.systemKey)).toEqual([
48
95
  "featured",
49
- "collections",
50
96
  "archive",
51
- "rss",
97
+ "collections",
98
+ "subscribe",
52
99
  "settings",
53
100
  ]);
54
101
  });
@@ -61,35 +108,150 @@ describe("Setup bootstrap logic", () => {
61
108
  expect(onboardingRow?.value).toBe("completed");
62
109
  });
63
110
 
64
- it("stores the initial language, CJK font, and timezone during setup", async () => {
111
+ // The state the site rests in between the two setup screens. It has to be
112
+ // `provisioned` and not `pending`: that is what lets an author who lost the
113
+ // session in between sign back in and finish, rather than being bounced back
114
+ // to a form that would try to create their account a second time.
115
+ it("leaves the site provisioned and unnamed after the account step", async () => {
116
+ await services.bootstrap.provisionOwnerAccount({
117
+ ownerUserId: "usr_test-owner",
118
+ });
119
+
120
+ const rows = await services.db.select().from(settings);
121
+ expect(rows.find((row) => row.key === "ONBOARDING_STATUS")?.value).toBe(
122
+ "provisioned",
123
+ );
124
+ expect(rows.find((row) => row.key === "SITE_NAME")).toBeUndefined();
125
+
126
+ const navItemsList = await services.db.select().from(navItems);
127
+ expect(navItemsList).toHaveLength(5);
128
+ });
129
+
130
+ // The hosted path through the same method: the control plane named the site
131
+ // at creation, and the author answering the last screen must not blank it.
132
+ it("leaves an existing name alone when the last screen asked for none", async () => {
133
+ await runSetupBootstrap(services);
134
+ await services.bootstrap.completeSiteSetup(
135
+ { siteLanguage: "en" },
136
+ { oldLanguage: "en" },
137
+ );
138
+
139
+ const rows = await services.db.select().from(settings);
140
+ expect(rows.find((row) => row.key === "SITE_NAME")?.value).toBe(
141
+ "Jant Demo",
142
+ );
143
+ });
144
+
145
+ it("renames the operator's account to match the site", async () => {
146
+ const updateCurrentUserName = vi.fn(async () => undefined);
147
+
148
+ await services.bootstrap.provisionOwnerAccount({
149
+ ownerUserId: "usr_test-owner",
150
+ });
151
+ await services.bootstrap.completeSiteSetup(
152
+ { siteName: " Jant Demo " },
153
+ { oldLanguage: "" },
154
+ { updateCurrentUserName },
155
+ );
156
+
157
+ expect(updateCurrentUserName).toHaveBeenCalledWith("Jant Demo");
158
+ });
159
+
160
+ // The rename runs before onboarding closes, so a better-auth failure leaves
161
+ // the author on the screen they were already on instead of inside a finished
162
+ // site under a stale account name.
163
+ it("leaves setup open when the account rename fails", async () => {
164
+ await services.bootstrap.provisionOwnerAccount({
165
+ ownerUserId: "usr_test-owner",
166
+ });
167
+ await expect(
168
+ services.bootstrap.completeSiteSetup(
169
+ { siteName: "Jant Demo" },
170
+ { oldLanguage: "" },
171
+ {
172
+ updateCurrentUserName: async () => {
173
+ throw new Error("better-auth unavailable");
174
+ },
175
+ },
176
+ ),
177
+ ).rejects.toThrow("better-auth unavailable");
178
+
179
+ const rows = await services.db.select().from(settings);
180
+ expect(rows.find((row) => row.key === "ONBOARDING_STATUS")?.value).toBe(
181
+ "provisioned",
182
+ );
183
+ });
184
+
185
+ it("stores the chosen content language and timezone during setup", async () => {
65
186
  await runSetupBootstrap(services, {
66
187
  siteLanguage: "en",
67
- cjkSerifFont: "zh-Hans",
68
188
  timeZone: "Asia/Shanghai",
69
189
  });
70
190
 
71
191
  const rows = await services.db.select().from(settings);
72
192
  expect(rows.find((row) => row.key === "SITE_LANGUAGE")?.value).toBe("en");
73
- expect(rows.find((row) => row.key === "CJK_SERIF_FONT")?.value).toBe(
74
- "zh-Hans",
75
- );
76
193
  expect(rows.find((row) => row.key === "TIME_ZONE")?.value).toBe(
77
194
  "Asia/Shanghai",
78
195
  );
79
196
  });
80
197
 
81
- it("pins the dashboard language to the detected catalog locale", async () => {
198
+ it("pins the dashboard language when only the browser knows it", async () => {
199
+ // Someone running an English blog from a Chinese browser: English site,
200
+ // Chinese dashboard. Following the content language would miss this.
201
+ await runSetupBootstrap(services, {
202
+ siteLanguage: "en",
203
+ browserLanguage: "zh-TW",
204
+ });
205
+
206
+ const rows = await services.db.select().from(settings);
207
+ expect(rows.find((row) => row.key === "SITE_LANGUAGE")?.value).toBe("en");
208
+ expect(rows.find((row) => row.key === "DASHBOARD_LANGUAGE")?.value).toBe(
209
+ "zh-Hant",
210
+ );
211
+ });
212
+
213
+ it("leaves the dashboard following a content language the author chose", async () => {
214
+ // The reverse mismatch, and the common one: an English browser is what
215
+ // every unconfigured machine reports, so it is no evidence against the
216
+ // language just chosen by hand. The dashboard must not freeze to English.
217
+ await runSetupBootstrap(services, {
218
+ siteLanguage: "zh-Hans",
219
+ browserLanguage: "en",
220
+ });
221
+
222
+ const rows = await services.db.select().from(settings);
223
+ expect(rows.find((row) => row.key === "SITE_LANGUAGE")?.value).toBe(
224
+ "zh-Hans",
225
+ );
226
+ expect(
227
+ rows.find((row) => row.key === "DASHBOARD_LANGUAGE"),
228
+ ).toBeUndefined();
229
+ });
230
+
231
+ it("leaves the dashboard following when the browser sent none", async () => {
82
232
  await runSetupBootstrap(services, { siteLanguage: "zh-TW" });
83
233
 
84
234
  const rows = await services.db.select().from(settings);
85
- // Content language stays verbatim; the dashboard locale is the resolved
86
- // catalog (zh-Hant) so it is stable if content language later changes.
235
+ // Content language stays verbatim, and the dashboard derives zh-Hant from
236
+ // it at render time rather than being pinned here.
87
237
  expect(rows.find((row) => row.key === "SITE_LANGUAGE")?.value).toBe(
88
238
  "zh-TW",
89
239
  );
90
- expect(rows.find((row) => row.key === "DASHBOARD_LANGUAGE")?.value).toBe(
91
- "zh-Hant",
92
- );
240
+ expect(
241
+ rows.find((row) => row.key === "DASHBOARD_LANGUAGE"),
242
+ ).toBeUndefined();
243
+ });
244
+
245
+ it("leaves the dashboard following when browser and content agree", async () => {
246
+ await runSetupBootstrap(services, {
247
+ siteLanguage: "zh-Hans",
248
+ browserLanguage: "zh-CN",
249
+ });
250
+
251
+ const rows = await services.db.select().from(settings);
252
+ expect(
253
+ rows.find((row) => row.key === "DASHBOARD_LANGUAGE"),
254
+ ).toBeUndefined();
93
255
  });
94
256
 
95
257
  it("is idempotent when default navigation already exists", async () => {
@@ -141,5 +303,95 @@ describe("Setup bootstrap logic", () => {
141
303
  const domainRows = await services.db.select().from(siteDomains);
142
304
  expect(siteRows).toHaveLength(1);
143
305
  expect(domainRows).toHaveLength(0);
306
+
307
+ const rows = await services.db.select().from(settings);
308
+ expect(rows.find((row) => row.key === "SITE_NAME")?.siteId).toBe(
309
+ siteRows[0]?.id,
310
+ );
311
+ });
312
+
313
+ // Settings written under the placeholder would belong to no site at all.
314
+ it("refuses to finish a site the account step has not created", async () => {
315
+ await services.db.run(sql`DELETE FROM "site"`);
316
+
317
+ await expect(
318
+ createBootstrapService(
319
+ services.db,
320
+ TRANSIENT_SINGLE_SITE_ID,
321
+ ).completeSiteSetup({ siteName: "Jant Demo" }, { oldLanguage: "" }),
322
+ ).rejects.toThrow("completeSiteSetup needs an existing site");
323
+
324
+ expect(await services.db.select().from(settings)).toHaveLength(0);
325
+ });
326
+ });
327
+
328
+ // A hosted database holds every tenant, so "the only site" has no answer there.
329
+ // Setup's last screen must write to the site the request's host resolved — the
330
+ // one the route's membership check just read — and leave every other tenant
331
+ // alone. The account screen has no hosted use at all.
332
+ describe("Setup bootstrap on a host-based install", () => {
333
+ function createHostedServices() {
334
+ const { services, db } = createTestApp({
335
+ siteResolutionMode: "host-based",
336
+ });
337
+ return { services, db };
338
+ }
339
+
340
+ it("finishes the request's site while other sites share the database", async () => {
341
+ const { services, db } = createHostedServices();
342
+ const otherSiteId = await insertOtherSite(db);
343
+
344
+ await services.bootstrap.completeSiteSetup(
345
+ { siteLanguage: "en" },
346
+ { oldLanguage: "" },
347
+ );
348
+
349
+ const rows = await db.select().from(settings);
350
+ expect(
351
+ rows.find(
352
+ (row) =>
353
+ row.siteId === DEFAULT_TEST_SITE_ID &&
354
+ row.key === "ONBOARDING_STATUS",
355
+ )?.value,
356
+ ).toBe("completed");
357
+ expect(rows.filter((row) => row.siteId === otherSiteId)).toHaveLength(0);
358
+ });
359
+
360
+ // A hosted site's owner arrives through the control plane's handoff. The
361
+ // account step would make whoever signed in the owner of the site the host
362
+ // resolved, so it refuses before writing anything to any tenant.
363
+ it("refuses the account step and writes nothing", async () => {
364
+ const { services, db } = createHostedServices();
365
+ await insertOtherSite(db);
366
+
367
+ await expect(
368
+ services.bootstrap.provisionOwnerAccount({
369
+ ownerUserId: "usr_test-owner",
370
+ }),
371
+ ).rejects.toThrow(
372
+ "provisionOwnerAccount runs on self-hosted installs only",
373
+ );
374
+
375
+ expect(await db.select().from(sites)).toHaveLength(2);
376
+ expect(await db.select().from(siteMembers)).toHaveLength(0);
377
+ expect(await db.select().from(navItems)).toHaveLength(0);
378
+ expect(await db.select().from(settings)).toHaveLength(0);
379
+ });
380
+
381
+ // The control plane creates hosted sites. Materializing one here would add a
382
+ // stray tenant to a database that holds real ones.
383
+ it("never creates a site when the request resolved none", async () => {
384
+ const db = createTestDatabase().db as unknown as Database;
385
+ await db.run(sql`DELETE FROM "site"`);
386
+
387
+ await expect(
388
+ createBootstrapService(db, TRANSIENT_SINGLE_SITE_ID, {
389
+ siteResolutionMode: "host-based",
390
+ }).provisionOwnerAccount({ ownerUserId: "usr_test-owner" }),
391
+ ).rejects.toThrow(
392
+ "provisionOwnerAccount runs on self-hosted installs only",
393
+ );
394
+
395
+ expect(await db.select().from(sites)).toHaveLength(0);
144
396
  });
145
397
  });