@jant/core 0.6.16 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -99,6 +99,79 @@ function filesToMap(
99
99
  }
100
100
 
101
101
  describe("createExportService (Hugo)", () => {
102
+ // Hugo has its own summary rule and would cut somewhere else, so whether the
103
+ // timeline truncates a post is decided here and carried as front matter.
104
+ // Written only when true — a consumer reads its presence.
105
+ it("marks a truncated post in front matter and leaves a short one unmarked", async () => {
106
+ const para = (text: string) => ({
107
+ type: "paragraph",
108
+ content: [{ type: "text", text }],
109
+ });
110
+ const long = makePost({
111
+ id: "post-long",
112
+ slug: "long-read",
113
+ title: "A long read",
114
+ threadId: "post-long",
115
+ body: JSON.stringify({
116
+ type: "doc",
117
+ content: [
118
+ para("Alpha ".repeat(90).trim()),
119
+ para("Omega ".repeat(90).trim()),
120
+ ],
121
+ }),
122
+ });
123
+ const short = makePost({
124
+ id: "post-short",
125
+ slug: "short-note",
126
+ title: null,
127
+ threadId: "post-short",
128
+ body: JSON.stringify({ type: "doc", content: [para("Short enough.")] }),
129
+ });
130
+
131
+ const files = await createExportService(
132
+ {
133
+ posts: { list: async () => [long, short] },
134
+ paths: {
135
+ getPostSlugMap: async () =>
136
+ new Map([
137
+ ["post-long", "long-read"],
138
+ ["post-short", "short-note"],
139
+ ]),
140
+ getPostAliases: async () => new Map(),
141
+ getCollectionSlugMap: async () => new Map(),
142
+ },
143
+ collections: {
144
+ list: async () => [],
145
+ listDirectoryData: async () => ({
146
+ collections: [],
147
+ items: [],
148
+ directoryItems: [],
149
+ }),
150
+ getCollectionsByPostIds: async () => new Map(),
151
+ getCollectionEntriesByThreadIds: async () => new Map(),
152
+ },
153
+ media: { getByPostIds: async () => new Map() },
154
+ } as unknown as ServicesArg,
155
+ makeSiteConfig(),
156
+ ).generateHugoFiles();
157
+
158
+ const read = async (path: string) => {
159
+ const file = files.find((f) => f.path === path);
160
+ expect(file, `missing ${path}`).toBeDefined();
161
+ const { frontMatter } = await parseFrontMatter(
162
+ typeof file!.content === "string"
163
+ ? file!.content
164
+ : new TextDecoder().decode(file!.content),
165
+ );
166
+ return frontMatter;
167
+ };
168
+
169
+ expect((await read("content/long-read/_index.md")).truncated).toBe(true);
170
+ expect(
171
+ (await read("content/short-note/_index.md")).truncated,
172
+ ).toBeUndefined();
173
+ });
174
+
102
175
  it("emits a branch bundle per root post with YAML front matter in stable key order", async () => {
103
176
  const root = makePost({
104
177
  id: "post-root",
@@ -496,6 +569,164 @@ describe("createExportService (Hugo)", () => {
496
569
  expect(data).toContain('url = "/index.xml"');
497
570
  });
498
571
 
572
+ it("resolves the nav Subscribe link to the feed the page would recommend", async () => {
573
+ const service = createExportService(
574
+ buildServices({ posts: [] }),
575
+ makeSiteConfig({
576
+ mainRssFeed: "featured",
577
+ navItems: [
578
+ {
579
+ type: "system",
580
+ systemKey: "subscribe",
581
+ label: "",
582
+ url: "/subscribe",
583
+ position: 0,
584
+ placement: "more",
585
+ },
586
+ ],
587
+ }),
588
+ );
589
+ const data = filesToMap(await service.generateHugoFiles()).get(
590
+ "data/jant.toml",
591
+ ) as string;
592
+
593
+ // There is no /subscribe page in the exported site to link at.
594
+ expect(data).not.toContain('url = "/subscribe"');
595
+ expect(data).toContain('url = "/featured/index.xml"');
596
+ expect(data).toContain('label = "Subscribe"');
597
+ });
598
+
599
+ it("redirects every runtime feed address to its exported counterpart", async () => {
600
+ const service = createExportService(
601
+ buildServices({ posts: [] }),
602
+ makeSiteConfig({ mainRssFeed: "latest" }),
603
+ );
604
+ const redirects = filesToMap(await service.generateHugoFiles()).get(
605
+ "static/_redirects",
606
+ ) as string;
607
+
608
+ // Feed readers hold the runtime addresses; nothing in the Hugo tree
609
+ // answers them without these.
610
+ const rules = redirects
611
+ .split("\n")
612
+ .filter((l) => l.startsWith("/"))
613
+ .map((l) => l.split(/\s+/));
614
+ expect(new Map(rules.map((r) => [r[0], r[1]]))).toEqual(
615
+ new Map([
616
+ ["/feed", "/index.xml"],
617
+ ["/latest/feed", "/index.xml"],
618
+ ["/featured/feed", "/featured/index.xml"],
619
+ ["/archive/feed", "/archive/index.xml"],
620
+ // The 308 aliases the live site still answers.
621
+ ["/feed/latest", "/index.xml"],
622
+ ["/feed/all", "/index.xml"],
623
+ ["/feed/featured", "/featured/index.xml"],
624
+ ["/feed/atom.xml", "/index.xml"],
625
+ ["/feed/latest/atom.xml", "/index.xml"],
626
+ ["/feed/all/atom.xml", "/index.xml"],
627
+ ["/feed/featured/atom.xml", "/featured/index.xml"],
628
+ ["/:slug/feed", "/:slug/index.xml"],
629
+ ]),
630
+ );
631
+ // A feed reader follows a 301 and rewrites its stored address; a 302
632
+ // would leave it polling the dead one forever.
633
+ expect(rules.every((r) => r[2] === "301")).toBe(true);
634
+ // First match wins in both hosts that read this file, so the collection
635
+ // catch-all must not shadow the fixed sections.
636
+ expect(rules.at(-1)?.[0]).toBe("/:slug/feed");
637
+ });
638
+
639
+ it("points /feed at the featured feed when that is the site's main feed", async () => {
640
+ const service = createExportService(
641
+ buildServices({ posts: [] }),
642
+ makeSiteConfig({ mainRssFeed: "featured" }),
643
+ );
644
+ const redirects = filesToMap(await service.generateHugoFiles()).get(
645
+ "static/_redirects",
646
+ ) as string;
647
+
648
+ expect(redirects).toMatch(/^\/feed\s+\/featured\/index\.xml\s+301$/m);
649
+ expect(redirects).toMatch(
650
+ /^\/feed\/atom\.xml\s+\/featured\/index\.xml\s+301$/m,
651
+ );
652
+ });
653
+
654
+ it("falls back to the latest feed when no featured section was emitted", async () => {
655
+ // A post owning the `featured` slug takes the section's place, so
656
+ // /featured/index.xml is never built. Redirecting there would turn a live
657
+ // subscription into a 404; the latest feed is wider but real.
658
+ const service = createExportService(
659
+ buildServices({ posts: [makePost({ id: "post-1", slug: "featured" })] }),
660
+ makeSiteConfig({ mainRssFeed: "featured" }),
661
+ );
662
+ const files = filesToMap(await service.generateHugoFiles());
663
+ const redirects = files.get("static/_redirects") as string;
664
+
665
+ // The post took the path the featured section would have used, so the
666
+ // section — and /featured/index.xml with it — was never built.
667
+ expect(files.get("content/featured/_index.md") as string).not.toContain(
668
+ "type: featured",
669
+ );
670
+ expect(redirects).not.toContain("/featured/index.xml");
671
+ expect(redirects).toMatch(/^\/feed\s+\/index\.xml\s+301$/m);
672
+ expect(redirects).toMatch(/^\/featured\/feed\s+\/index\.xml\s+301$/m);
673
+ });
674
+
675
+ it("writes no feed redirects when the site publishes no feeds", async () => {
676
+ const service = createExportService(
677
+ buildServices({ posts: [] }),
678
+ makeSiteConfig({ rssFeedsEnabled: false }),
679
+ );
680
+ const files = filesToMap(await service.generateHugoFiles());
681
+
682
+ expect(files.has("static/_redirects")).toBe(false);
683
+ expect(files.get("README.md") as string).not.toContain("## Feeds");
684
+ });
685
+
686
+ it("tells the README reader that feed addresses are the exception to URL continuity", async () => {
687
+ const service = createExportService(
688
+ buildServices({ posts: [] }),
689
+ makeSiteConfig({ mainRssFeed: "latest" }),
690
+ );
691
+ const readme = filesToMap(await service.generateHugoFiles()).get(
692
+ "README.md",
693
+ ) as string;
694
+
695
+ // The Notes list advertises that post URLs survive the move; without this
696
+ // it reads as a promise covering every URL.
697
+ expect(readme).toContain("## Feeds");
698
+ expect(readme).toContain("`/latest/feed`");
699
+ expect(readme).toContain("static/_redirects");
700
+ expect(readme).toContain("Feed addresses are the exception");
701
+ // No /subscribe page exists in the export, and the README says where the
702
+ // nav entry lands instead.
703
+ expect(readme).toContain("has no `/subscribe` page");
704
+ });
705
+
706
+ it("hides system Subscribe navigation when feeds are off", async () => {
707
+ const service = createExportService(
708
+ buildServices({ posts: [] }),
709
+ makeSiteConfig({
710
+ rssFeedsEnabled: false,
711
+ navItems: [
712
+ {
713
+ type: "system",
714
+ systemKey: "subscribe",
715
+ label: "System subscribe",
716
+ url: "/subscribe",
717
+ position: 0,
718
+ placement: "more",
719
+ },
720
+ ],
721
+ }),
722
+ );
723
+ const data = filesToMap(await service.generateHugoFiles()).get(
724
+ "data/jant.toml",
725
+ ) as string;
726
+
727
+ expect(data).not.toContain('label = "System subscribe"');
728
+ });
729
+
499
730
  it("emits last_activity_at on root post front matter so feed <updated> tracks thread bumps", async () => {
500
731
  const post = makePost({
501
732
  id: "post-1",
@@ -0,0 +1,91 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ /**
5
+ * `src/styles`, resolved from the working directory rather than
6
+ * `import.meta.url`: under the `happy-dom` environment `import.meta.url` is an
7
+ * `http:` URL that `readFileSync` rejects, and Vitest runs with the package
8
+ * root as its working directory in every environment.
9
+ */
10
+ const STYLES_DIR = resolve(process.cwd(), "src/styles");
11
+
12
+ /** Hand-written component CSS a signed-out visitor downloads. */
13
+ const READER_SHEETS = ["ui.css", "components.css"] as const;
14
+
15
+ /** Hand-written component CSS that only loads on an authenticated page. */
16
+ const AUTHOR_SHEETS = ["ui-author.css", "components-author.css"] as const;
17
+
18
+ function read(names: readonly string[]): string {
19
+ return names
20
+ .map((name) => readFileSync(resolve(STYLES_DIR, name), "utf8"))
21
+ .join("\n");
22
+ }
23
+
24
+ /**
25
+ * Every `@layer components` rule Jant hand-writes, both audiences together.
26
+ *
27
+ * `ui.css`/`components.css` carry what readers need; `ui-author.css` and
28
+ * `components-author.css` carry the composer, editor, settings, config editor
29
+ * and dash chrome that only load once someone is signed in. A test asserting
30
+ * that a rule is declared cares that it exists at all, not which half it
31
+ * landed in — so moving a rule between them must never break a test.
32
+ *
33
+ * @returns The concatenated contents of all four stylesheets.
34
+ * @example
35
+ * expect(readComponentCss()).toMatch(/\.compose-post-meta-panel\s*\{/);
36
+ */
37
+ export function readComponentCss(): string {
38
+ return read([...READER_SHEETS, ...AUTHOR_SHEETS]);
39
+ }
40
+
41
+ /**
42
+ * Only the reader halves.
43
+ *
44
+ * Use when the point of the assertion is that a rule ships to signed-out
45
+ * visitors — or, with a negative matcher, that it does not.
46
+ *
47
+ * @returns The concatenated contents of `ui.css` and `components.css`.
48
+ * @example
49
+ * expect(readReaderCss()).not.toMatch(/\.compose-dialog\s*\{/);
50
+ */
51
+ export function readReaderCss(): string {
52
+ return read(READER_SHEETS);
53
+ }
54
+
55
+ /**
56
+ * Only the author halves.
57
+ *
58
+ * @returns The concatenated contents of `ui-author.css` and
59
+ * `components-author.css`.
60
+ * @example
61
+ * expect(readAuthorCss()).toMatch(/\.settings-root\s*\{/);
62
+ */
63
+ export function readAuthorCss(): string {
64
+ return read(AUTHOR_SHEETS);
65
+ }
66
+
67
+ /**
68
+ * Every stylesheet Jant hand-writes, each paired with its filename.
69
+ *
70
+ * `readComponentCss()` and friends concatenate, which is right for asking
71
+ * whether a rule exists. This one keeps the files apart so a failure can name
72
+ * the file and line it found the problem on — `tokens.css` and `preset.css`
73
+ * are in here too, because breakpoints live in all six.
74
+ *
75
+ * @returns One entry per stylesheet, in load order.
76
+ * @example
77
+ * for (const { name, css } of readAllStylesheets()) {
78
+ * expect(css, name).not.toMatch(/@media \(max-width: 767px\)/);
79
+ * }
80
+ */
81
+ export function readAllStylesheets(): readonly {
82
+ name: string;
83
+ css: string;
84
+ }[] {
85
+ return [
86
+ { name: "preset.css", path: resolve(process.cwd(), "src/preset.css") },
87
+ ...[...READER_SHEETS, ...AUTHOR_SHEETS, "tokens.css", "site-media.css"].map(
88
+ (name) => ({ name, path: resolve(STYLES_DIR, name) }),
89
+ ),
90
+ ].map(({ name, path }) => ({ name, css: readFileSync(path, "utf8") }));
91
+ }
@@ -134,3 +134,36 @@ export function createTestDatabase(options?: { fts?: boolean }) {
134
134
 
135
135
  return { db, sqlite };
136
136
  }
137
+
138
+ /** The most bound parameters Cloudflare D1 accepts in one statement. */
139
+ export const D1_MAX_BOUND_PARAMETERS = 100;
140
+
141
+ /**
142
+ * Make a test database refuse what D1 refuses: a statement with more than
143
+ * {@link D1_MAX_BOUND_PARAMETERS} bound parameters.
144
+ *
145
+ * better-sqlite3 accepts tens of thousands, so a query that puts every id in
146
+ * one `IN (…)` list passes every test and fails in production once the list
147
+ * grows. Install this after seeding, so only the code under test is held to it.
148
+ *
149
+ * @param sqlite - The handle `createTestDatabase` or `createTestApp` returned
150
+ * @returns Nothing; the handle is changed in place
151
+ * @example
152
+ * ```ts
153
+ * const { app, sqlite } = createTestApp();
154
+ * await seed(app);
155
+ * enforceD1BoundParameterLimit(sqlite);
156
+ * ```
157
+ */
158
+ export function enforceD1BoundParameterLimit(sqlite: Database.Database): void {
159
+ const prepare = sqlite.prepare.bind(sqlite);
160
+ sqlite.prepare = ((source: string) => {
161
+ const count = source.match(/\?/g)?.length ?? 0;
162
+ if (count > D1_MAX_BOUND_PARAMETERS) {
163
+ throw new Error(
164
+ `too many SQL variables: ${count} bound parameters, D1 allows ${D1_MAX_BOUND_PARAMETERS}`,
165
+ );
166
+ }
167
+ return prepare(source);
168
+ }) as Database.Database["prepare"];
169
+ }
@@ -87,10 +87,13 @@ export function makeSiteConfig(over: Partial<SiteConfig> = {}): SiteConfig {
87
87
  siteUrl: "https://example.com",
88
88
  siteDescription: "Export fixture",
89
89
  siteLanguage: "en",
90
+ multilingualEnabled: false,
91
+ additionalLanguages: [],
90
92
  showJantBrandingOnHome: true,
91
93
  publicApiEnabled: true,
92
94
  rssFeedsEnabled: true,
93
95
  mainRssFeed: "latest",
96
+ archiveDefaultLayout: "list",
94
97
  siteFooter: "",
95
98
  showHeaderAvatar: false,
96
99
  siteAvatarUrl: "",
@@ -0,0 +1,159 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ readAuthorCss,
4
+ readComponentCss,
5
+ readReaderCss,
6
+ } from "./helpers/component-css.js";
7
+
8
+ /**
9
+ * `client.css` is what a signed-out visitor waits on before the page paints,
10
+ * and `styles/ui.css` plus `styles/components.css` are the hand-written half
11
+ * of it. Author-only component CSS belongs in `styles/ui-author.css` and
12
+ * `styles/components-author.css`, which `BaseLayout` links only on
13
+ * authenticated pages.
14
+ *
15
+ * Class families that only ever appear in authenticated markup. The editor's
16
+ * `.tiptap-*` chrome is here too, minus the names TipTap also writes into
17
+ * published post HTML — readers see those.
18
+ */
19
+ const AUTHOR_ONLY = [
20
+ "compose",
21
+ "command",
22
+ "settings",
23
+ "picker",
24
+ "confirm",
25
+ "draft",
26
+ "theme-preview",
27
+ // The floating compose button on phones, and the page modifier that keeps
28
+ // room for it. Reader layout renders both, but only for a signed-in author.
29
+ "site-mobile-compose",
30
+ "site-page-mobile-compose-enabled",
31
+ // From `components.css`: the config editor, the navigation manager, the
32
+ // custom URL manager, the dash chrome and the form skeletons. `.nav-*` here
33
+ // is `jant-nav-manager`, not the site header — that is `.site-header-nav`.
34
+ // `.collection-picker-*` is spelled out because plain `.collection-*` is a
35
+ // reader family (the collection pages).
36
+ "config",
37
+ "nav",
38
+ "custom",
39
+ "dash",
40
+ "skel",
41
+ "collection-picker",
42
+ ];
43
+
44
+ /** TipTap class names that land in reader-rendered post HTML. */
45
+ const READER_TIPTAP = [
46
+ "tiptap-table",
47
+ "tiptap-html-block",
48
+ "tiptap-more-break",
49
+ "tiptap-embed-figure",
50
+ "tiptap-embed-frame",
51
+ "tiptap-embed-fallback",
52
+ "tiptap-footnote-reference",
53
+ "tiptap-footnote-definition",
54
+ ];
55
+
56
+ /**
57
+ * `.compose-thread-layout` shares the compose rail's marker vocabulary with
58
+ * the reading rail's `.thread-group` in one declaration, so that the two
59
+ * cannot drift apart. That single grouped selector is the documented
60
+ * exception; see the comment on the rule in `styles/ui.css`.
61
+ */
62
+ const READER_SHEET_EXCEPTIONS = [".compose-thread-layout"];
63
+
64
+ /** Every class named in a selector, ignoring declaration bodies. */
65
+ function selectorClasses(css: string): string[] {
66
+ const found: string[] = [];
67
+ // Selectors are what precedes a `{` that opens a block; declarations end in
68
+ // `;`, so anything after the last `;` or `}` is the selector text.
69
+ for (const chunk of css.split("{")) {
70
+ const selector = chunk.slice(
71
+ Math.max(chunk.lastIndexOf(";"), chunk.lastIndexOf("}")) + 1,
72
+ );
73
+ const withoutComments = selector.replace(/\/\*[\s\S]*?\*\//g, " ");
74
+ for (const match of withoutComments.matchAll(
75
+ /\.(-?[A-Za-z_][A-Za-z0-9_-]*)/g,
76
+ )) {
77
+ found.push(match[1]!);
78
+ }
79
+ }
80
+ return found;
81
+ }
82
+
83
+ describe("stylesheet audience", () => {
84
+ it("keeps author-only component CSS out of the reader stylesheet", () => {
85
+ const css = readReaderCss();
86
+ const exceptions = new Set(
87
+ READER_SHEET_EXCEPTIONS.map((name) => name.replace(/^\./, "")),
88
+ );
89
+
90
+ const leaked = [
91
+ ...new Set(
92
+ selectorClasses(css).filter(
93
+ (name) =>
94
+ !exceptions.has(name) &&
95
+ AUTHOR_ONLY.some(
96
+ (family) => name === family || name.startsWith(`${family}-`),
97
+ ),
98
+ ),
99
+ ),
100
+ ].sort();
101
+
102
+ expect(leaked).toEqual([]);
103
+ });
104
+
105
+ it("keeps the editor chrome out of the reader stylesheet", () => {
106
+ const readerTiptap = new Set(READER_TIPTAP);
107
+ const leaked = [
108
+ ...new Set(
109
+ selectorClasses(readReaderCss()).filter(
110
+ (name) =>
111
+ (name === "tiptap" || name.startsWith("tiptap-")) &&
112
+ !readerTiptap.has(name),
113
+ ),
114
+ ),
115
+ ].sort();
116
+
117
+ expect(leaked).toEqual([]);
118
+ });
119
+
120
+ it("still styles the TipTap output a published post carries", () => {
121
+ // `lib/embed-render.ts` and `lib/markdown-manager.ts` write these into post
122
+ // HTML, and they are the ones with rules of their own — a reader holding
123
+ // only `client.css` has to be able to style them. The rest of
124
+ // READER_TIPTAP reaches published posts too but is styled by element
125
+ // through `.prose`, so there is no rule here to keep.
126
+ const css = readReaderCss();
127
+ for (const name of [
128
+ "tiptap-embed-figure",
129
+ "tiptap-embed-frame",
130
+ "tiptap-embed-fallback",
131
+ "tiptap-more-break",
132
+ ]) {
133
+ expect(selectorClasses(css)).toContain(name);
134
+ }
135
+ });
136
+
137
+ it("moved real weight, not a handful of rules", () => {
138
+ // The split is only worth its complexity while the author half is
139
+ // substantial. If it ever shrinks to a rounding error, fold it back in.
140
+ expect(readAuthorCss().length).toBeGreaterThan(readReaderCss().length / 2);
141
+ });
142
+
143
+ it("keeps the author half of components.css out of the reader sheet", () => {
144
+ // `preset.css` imports `components.css` on every page, so the settings
145
+ // pages and the config editor must not be left in it.
146
+ const author = readAuthorCss();
147
+ expect(author).toContain(".settings-root");
148
+ expect(author).toContain(".config-editor-page");
149
+ });
150
+
151
+ it("leaves the reader-side families of components.css in place", () => {
152
+ // Verified against every `class=` usage in the tree: `BaseLayout` renders
153
+ // the toast container on every page, and `.page-intro-*` is the header of
154
+ // the archive and collections pages, both public.
155
+ const reader = readReaderCss();
156
+ expect(reader).toContain(".toast-container");
157
+ expect(reader).toContain(".page-intro");
158
+ });
159
+ });