@jant/core 0.6.16 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,270 @@
1
+ /**
2
+ * The whole self-hosted setup, walked once with nothing mocked.
3
+ *
4
+ * Real services, a real better-auth, a real database: the other setup tests
5
+ * each hold one seam still, and this one is here to catch what only shows up
6
+ * when they are all moving — the session the first screen mints being the one
7
+ * the second screen accepts, the status the first screen writes being the one
8
+ * the second screen reads, and the site ending up named after neither of them
9
+ * guessed.
10
+ */
11
+
12
+ import { describe, expect, it, vi } from "vitest";
13
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
14
+ import { createAuth } from "../../../auth.js";
15
+ import { attachSession } from "../../../middleware/session.js";
16
+ import {
17
+ settings as settingsTable,
18
+ siteMembers,
19
+ user,
20
+ } from "../../../db/schema.js";
21
+ import { setupRoutes } from "../setup.js";
22
+
23
+ const PASSWORD = "correct-horse-battery";
24
+
25
+ function createSetupApp(options: Parameters<typeof createTestApp>[0] = {}) {
26
+ const { app, services, db } = createTestApp(options);
27
+
28
+ // The helper installs a stub that answers every session question the same
29
+ // way. Setup needs the real thing: it mints a session on the first screen and
30
+ // has to be believed on the second.
31
+ const auth = createAuth(db, {
32
+ secret: "test-secret-with-enough-entropy-for-session-cookies",
33
+ baseURL: "http://localhost",
34
+ useSecureCookies: false,
35
+ });
36
+ app.use("*", async (c, next) => {
37
+ c.set("auth", auth);
38
+ await next();
39
+ });
40
+ app.use("*", attachSession());
41
+ app.route("/", setupRoutes);
42
+
43
+ return { app, services, db, auth };
44
+ }
45
+
46
+ function post(
47
+ app: ReturnType<typeof createSetupApp>["app"],
48
+ body: Record<string, unknown>,
49
+ cookie?: string,
50
+ ) {
51
+ return app.request("/setup", {
52
+ method: "POST",
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ ...(cookie ? { Cookie: cookie } : {}),
56
+ },
57
+ body: JSON.stringify(body),
58
+ });
59
+ }
60
+
61
+ /** The session cookie a response minted, in the form a request sends back. */
62
+ function sessionCookie(res: Response): string {
63
+ const cookie = res.headers
64
+ .getSetCookie()
65
+ .find((value) => value.includes("session_token="));
66
+ expect(cookie).toBeDefined();
67
+ return cookie!.split(";")[0]!;
68
+ }
69
+
70
+ async function readSettings(db: ReturnType<typeof createSetupApp>["db"]) {
71
+ const rows = await db.select().from(settingsTable);
72
+ return Object.fromEntries(rows.map((row) => [row.key, row.value]));
73
+ }
74
+
75
+ describe("self-hosted setup, both screens", () => {
76
+ it("walks an empty install to a finished site", async () => {
77
+ const { app, db } = createSetupApp();
78
+
79
+ const first = await app.request("/setup");
80
+ await expect(first.text()).resolves.toContain("Setup · Step 1 of 2");
81
+
82
+ const account = await post(app, {
83
+ email: "owner@example.com",
84
+ password: PASSWORD,
85
+ });
86
+ const cookie = sessionCookie(account);
87
+
88
+ // Between the screens: an owner, a navigation profile, and no answers.
89
+ const midway = await readSettings(db);
90
+ expect(midway["ONBOARDING_STATUS"]).toBe("provisioned");
91
+ expect(midway["SITE_NAME"]).toBeUndefined();
92
+
93
+ // The session the first screen minted is the one the second screen trusts.
94
+ const second = await app.request("/setup", { headers: { Cookie: cookie } });
95
+ const secondHtml = await second.text();
96
+ expect(secondHtml).toContain("Setup · Step 2 of 2");
97
+ expect(secondHtml).toContain("setup-site-name");
98
+
99
+ await post(
100
+ app,
101
+ {
102
+ siteName: "My Blog",
103
+ contentLanguage: "zh-Hans",
104
+ language: "en-GB",
105
+ timezone: "Asia/Shanghai",
106
+ discover: true,
107
+ },
108
+ cookie,
109
+ );
110
+
111
+ const finished = await readSettings(db);
112
+ expect(finished["ONBOARDING_STATUS"]).toBe("completed");
113
+ expect(finished["SITE_NAME"]).toBe("My Blog");
114
+ expect(finished["SITE_LANGUAGE"]).toBe("zh-Hans");
115
+ expect(finished["TIME_ZONE"]).toBe("Asia/Shanghai");
116
+ expect(finished["DISCOVER"]).toBe("latest");
117
+ // Left following the content language. An English browser is what every
118
+ // unconfigured machine reports, so it is no evidence against the language
119
+ // the author just chose by hand one field above.
120
+ expect(finished["DASHBOARD_LANGUAGE"]).toBeUndefined();
121
+
122
+ // The account opened under the address, and the second screen renamed it.
123
+ const [owner] = await db.select().from(user);
124
+ expect(owner?.name).toBe("My Blog");
125
+ expect(owner?.email).toBe("owner@example.com");
126
+ });
127
+
128
+ // A finished site must never offer the screen that opens the first account
129
+ // again, to anyone, signed in or not.
130
+ it("closes setup once the site is finished", async () => {
131
+ const { app } = createSetupApp();
132
+
133
+ const cookie = sessionCookie(
134
+ await post(app, { email: "owner@example.com", password: PASSWORD }),
135
+ );
136
+ await post(app, { siteName: "My Blog", contentLanguage: "en" }, cookie);
137
+
138
+ const page = await app.request("/setup", {
139
+ headers: { Cookie: cookie },
140
+ redirect: "manual",
141
+ });
142
+ expect(page.status).toBe(302);
143
+ expect(page.headers.get("Location")).toBe("/");
144
+
145
+ const submitted = await post(
146
+ app,
147
+ { email: "someone@example.com", password: PASSWORD },
148
+ cookie,
149
+ );
150
+ expect(submitted.status).toBe(302);
151
+ });
152
+
153
+ // Losing the session between the screens is the failure the split invented.
154
+ // The site is provisioned, so the setup screen refuses an unauthenticated
155
+ // visitor — and `/signin` stays open for them, which is how they get back.
156
+ it("hides the second screen from a lost session", async () => {
157
+ const { app } = createSetupApp();
158
+
159
+ await post(app, { email: "owner@example.com", password: PASSWORD });
160
+
161
+ const res = await app.request("/setup", { redirect: "manual" });
162
+
163
+ expect(res.status).toBe(302);
164
+ expect(res.headers.get("Location")).toBe("/");
165
+ });
166
+
167
+ // The failure that used to brick an install: the account is opened, standing
168
+ // the site up then fails, and the site is left `pending` with a user row in
169
+ // it. From there `createAuth` refuses every signup and the onboarding
170
+ // middleware sends `/signin` and `/reset` back to this screen — so retrying
171
+ // the screen has to be the way out.
172
+ it("resumes a first screen that failed after opening the account", async () => {
173
+ const { app, services, db } = createSetupApp();
174
+ const provision = vi.spyOn(services.bootstrap, "provisionOwnerAccount");
175
+ provision.mockRejectedValueOnce(new Error("D1 unavailable"));
176
+
177
+ const failed = await post(app, {
178
+ email: "owner@example.com",
179
+ password: PASSWORD,
180
+ });
181
+ await expect(failed.text()).resolves.toContain("Couldn't create your");
182
+
183
+ // The account outlived the failure, and the site did not advance.
184
+ expect((await db.select().from(user)).length).toBe(1);
185
+ expect(await services.settings.getOnboardingStatus()).toBe("pending");
186
+
187
+ // Registration is closed to everyone now, this address included — which is
188
+ // why the retry has to resume rather than create.
189
+ const resumed = await post(app, {
190
+ email: "owner@example.com",
191
+ password: PASSWORD,
192
+ });
193
+
194
+ expect(await services.settings.getOnboardingStatus()).toBe("provisioned");
195
+ const second = await app.request("/setup", {
196
+ headers: { Cookie: sessionCookie(resumed) },
197
+ });
198
+ await expect(second.text()).resolves.toContain("Setup · Step 2 of 2");
199
+ // Still one account: resuming picked the existing one up.
200
+ expect((await db.select().from(user)).length).toBe(1);
201
+ });
202
+
203
+ it("refuses to resume on the wrong password", async () => {
204
+ const { app, services, db } = createSetupApp();
205
+ vi.spyOn(services.bootstrap, "provisionOwnerAccount").mockRejectedValueOnce(
206
+ new Error("D1 unavailable"),
207
+ );
208
+
209
+ await post(app, { email: "owner@example.com", password: PASSWORD });
210
+ const res = await post(app, {
211
+ email: "owner@example.com",
212
+ password: "not-the-password",
213
+ });
214
+
215
+ await expect(res.text()).resolves.toContain("Couldn't create your");
216
+ expect(res.headers.getSetCookie()).toHaveLength(0);
217
+ expect(await services.settings.getOnboardingStatus()).toBe("pending");
218
+ });
219
+
220
+ // The reason a different address is not an escape hatch, and so the reason
221
+ // resuming had to be built: the registration hook counts rows, not emails.
222
+ it("will not open a second account to get around it", async () => {
223
+ const { app, services, db } = createSetupApp();
224
+ vi.spyOn(services.bootstrap, "provisionOwnerAccount").mockRejectedValueOnce(
225
+ new Error("D1 unavailable"),
226
+ );
227
+
228
+ await post(app, { email: "owner@example.com", password: PASSWORD });
229
+ const res = await post(app, {
230
+ email: "someone-else@example.com",
231
+ password: PASSWORD,
232
+ });
233
+
234
+ await expect(res.text()).resolves.toContain("Couldn't create your");
235
+ expect((await db.select().from(user)).length).toBe(1);
236
+ expect(await services.settings.getOnboardingStatus()).toBe("pending");
237
+ });
238
+ });
239
+
240
+ // A hosted site's owner arrives through the control plane's handoff, never
241
+ // through this screen. The control plane creates each hosted site already
242
+ // provisioned, so a `pending` one is a site that lost its status somehow — and
243
+ // the database behind it holds every tenant's accounts. Signing in here with
244
+ // any of them must not come away owning the site.
245
+ describe("setup on a host-based install", () => {
246
+ it("never makes an existing account the owner of a hosted site", async () => {
247
+ const { app, services, db, auth } = createSetupApp({
248
+ siteResolutionMode: "host-based",
249
+ });
250
+ // Another tenant's owner. Being the first account, it also closes
251
+ // registration, as it is closed on any hosted database.
252
+ await auth.api.signUpEmail({
253
+ body: {
254
+ name: "Other Tenant",
255
+ email: "other@example.com",
256
+ password: PASSWORD,
257
+ },
258
+ });
259
+
260
+ const res = await post(app, {
261
+ email: "other@example.com",
262
+ password: PASSWORD,
263
+ });
264
+
265
+ await expect(res.text()).resolves.toContain("Couldn't create your");
266
+ expect(res.headers.getSetCookie()).toHaveLength(0);
267
+ expect(await db.select().from(siteMembers)).toHaveLength(0);
268
+ expect(await services.settings.getOnboardingStatus()).toBe("pending");
269
+ });
270
+ });
@@ -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
+ });