@jant/core 0.3.27 → 0.3.29

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 (314) hide show
  1. package/bin/reset-password.js +22 -0
  2. package/dist/client/client.css +1 -0
  3. package/dist/client/client.js +31561 -0
  4. package/dist/index.js +15209 -15
  5. package/package.json +25 -15
  6. package/src/__tests__/helpers/app.ts +19 -3
  7. package/src/__tests__/helpers/db.ts +44 -0
  8. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  9. package/src/app.tsx +111 -174
  10. package/src/client.ts +13 -0
  11. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  12. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  13. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  14. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  15. package/src/db/schema.ts +24 -4
  16. package/src/i18n/locales/en.po +810 -385
  17. package/src/i18n/locales/en.ts +1 -1
  18. package/src/i18n/locales/zh-Hans.po +733 -522
  19. package/src/i18n/locales/zh-Hans.ts +1 -1
  20. package/src/i18n/locales/zh-Hant.po +733 -522
  21. package/src/i18n/locales/zh-Hant.ts +1 -1
  22. package/src/i18n/middleware.ts +7 -11
  23. package/src/index.ts +1 -1
  24. package/src/lib/__tests__/icons.test.ts +178 -0
  25. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  26. package/src/lib/__tests__/schemas.test.ts +12 -6
  27. package/src/lib/__tests__/theme.test.ts +62 -0
  28. package/src/lib/__tests__/timezones.test.ts +1 -1
  29. package/src/lib/__tests__/url.test.ts +12 -0
  30. package/src/lib/__tests__/view.test.ts +1 -5
  31. package/src/lib/avatar-upload.ts +18 -10
  32. package/src/lib/collection-form-bridge.ts +52 -0
  33. package/src/lib/collections-reorder.ts +28 -0
  34. package/src/lib/compose-bridge.ts +251 -0
  35. package/src/lib/errors.ts +116 -0
  36. package/src/lib/excerpt.ts +1 -1
  37. package/src/lib/favicon.ts +3 -5
  38. package/src/lib/html.ts +22 -0
  39. package/src/lib/icon-catalog.ts +181 -0
  40. package/src/lib/icons.ts +202 -0
  41. package/src/lib/navigation.ts +18 -33
  42. package/src/lib/pagination.ts +3 -2
  43. package/src/lib/post-form-bridge.ts +136 -0
  44. package/src/lib/render.tsx +11 -4
  45. package/src/lib/resolve-config.ts +157 -0
  46. package/src/lib/schemas.ts +76 -12
  47. package/src/lib/settings-bridge.ts +139 -0
  48. package/src/lib/storage.ts +37 -16
  49. package/src/lib/theme.ts +5 -7
  50. package/src/lib/timeline.ts +4 -8
  51. package/src/lib/toast.ts +134 -0
  52. package/src/lib/upload.ts +71 -0
  53. package/src/lib/url.ts +9 -1
  54. package/src/lib/version.ts +16 -0
  55. package/src/lib/view.ts +9 -10
  56. package/src/middleware/__tests__/auth.test.ts +6 -28
  57. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  58. package/src/middleware/auth.ts +6 -12
  59. package/src/middleware/config.ts +51 -0
  60. package/src/middleware/error-handler.ts +56 -0
  61. package/src/middleware/onboarding.ts +1 -1
  62. package/src/preset.css +6 -0
  63. package/src/routes/__tests__/compose.test.ts +104 -17
  64. package/src/routes/api/__tests__/collections.test.ts +93 -2
  65. package/src/routes/api/__tests__/posts.test.ts +2 -1
  66. package/src/routes/api/__tests__/settings.test.ts +1 -1
  67. package/src/routes/api/collections.ts +64 -68
  68. package/src/routes/api/nav-items.ts +21 -59
  69. package/src/routes/api/pages.ts +18 -46
  70. package/src/routes/api/posts.ts +64 -86
  71. package/src/routes/api/search.ts +6 -4
  72. package/src/routes/api/settings.ts +8 -24
  73. package/src/routes/api/upload.ts +55 -53
  74. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  75. package/src/routes/auth/reset.tsx +17 -66
  76. package/src/routes/auth/setup.tsx +67 -11
  77. package/src/routes/auth/signin.tsx +44 -8
  78. package/src/routes/compose.tsx +194 -0
  79. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  80. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  81. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  82. package/src/routes/dash/appearance.tsx +173 -0
  83. package/src/routes/dash/collections.tsx +80 -14
  84. package/src/routes/dash/index.tsx +12 -14
  85. package/src/routes/dash/media.tsx +46 -49
  86. package/src/routes/dash/pages.tsx +85 -37
  87. package/src/routes/dash/posts.tsx +60 -23
  88. package/src/routes/dash/redirects.tsx +43 -33
  89. package/src/routes/dash/settings.tsx +234 -214
  90. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  91. package/src/routes/feed/rss.ts +11 -16
  92. package/src/routes/feed/sitemap.ts +15 -9
  93. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  94. package/src/routes/pages/archive.tsx +2 -2
  95. package/src/routes/pages/collection.tsx +76 -9
  96. package/src/routes/pages/collections.tsx +3 -1
  97. package/src/routes/pages/featured.tsx +2 -2
  98. package/src/routes/pages/home.tsx +3 -3
  99. package/src/routes/pages/latest.tsx +2 -2
  100. package/src/routes/pages/page.tsx +2 -2
  101. package/src/routes/pages/post.tsx +2 -2
  102. package/src/routes/pages/search.tsx +2 -2
  103. package/src/services/__tests__/collection.test.ts +324 -34
  104. package/src/services/__tests__/media.test.ts +1 -1
  105. package/src/services/__tests__/page.test.ts +116 -1
  106. package/src/services/auth.ts +88 -0
  107. package/src/services/collection.ts +169 -30
  108. package/src/services/index.ts +8 -3
  109. package/src/services/media.ts +39 -12
  110. package/src/services/navigation.ts +17 -5
  111. package/src/services/page.ts +24 -4
  112. package/src/services/post.ts +87 -19
  113. package/src/services/search.ts +0 -1
  114. package/src/services/settings.ts +21 -13
  115. package/src/style.css +3 -0
  116. package/src/styles/components.css +42 -1
  117. package/src/styles/tokens.css +4 -0
  118. package/src/styles/ui.css +902 -73
  119. package/src/types/app-context.ts +25 -0
  120. package/src/types/bindings.ts +1 -0
  121. package/src/types/config.ts +60 -23
  122. package/src/types/entities.ts +12 -2
  123. package/src/types/lingui-react-macro.d.ts +3 -3
  124. package/src/types/operations.ts +2 -4
  125. package/src/types/views.ts +1 -3
  126. package/src/ui/__tests__/font-themes.test.ts +27 -8
  127. package/src/ui/color-themes.ts +1 -1
  128. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  129. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  130. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  131. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  132. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  133. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  134. package/src/ui/components/collection-types.ts +45 -0
  135. package/src/ui/components/compose-types.ts +75 -0
  136. package/src/ui/components/jant-collection-form.ts +512 -0
  137. package/src/ui/components/jant-compose-dialog.ts +494 -0
  138. package/src/ui/components/jant-compose-editor.ts +799 -0
  139. package/src/ui/components/jant-post-form.ts +290 -0
  140. package/src/ui/components/jant-settings-avatar.ts +231 -0
  141. package/src/ui/components/jant-settings-general.ts +436 -0
  142. package/src/ui/components/post-form-template.ts +260 -0
  143. package/src/ui/components/post-form-types.ts +87 -0
  144. package/src/ui/components/settings-types.ts +62 -0
  145. package/src/ui/compose/ComposeDialog.tsx +141 -385
  146. package/src/ui/compose/ComposePrompt.tsx +3 -3
  147. package/src/ui/dash/PostList.tsx +55 -61
  148. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  149. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  150. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  151. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  152. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  153. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  154. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  155. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  156. package/src/ui/dash/index.ts +1 -1
  157. package/src/ui/dash/posts/PostForm.tsx +248 -0
  158. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  159. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  160. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  161. package/src/ui/font-themes.ts +115 -32
  162. package/src/ui/layouts/BaseLayout.tsx +49 -19
  163. package/src/ui/layouts/DashLayout.tsx +14 -9
  164. package/src/ui/layouts/SiteLayout.tsx +38 -23
  165. package/src/ui/pages/CollectionPage.tsx +12 -2
  166. package/src/ui/pages/CollectionsPage.tsx +27 -27
  167. package/src/ui/pages/HomePage.tsx +15 -6
  168. package/src/ui/pages/SearchPage.tsx +1 -2
  169. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  170. package/src/ui/shared/Pagination.tsx +2 -2
  171. package/dist/app.js +0 -267
  172. package/dist/auth.js +0 -39
  173. package/dist/client.js +0 -13
  174. package/dist/db/index.js +0 -10
  175. package/dist/db/schema.js +0 -224
  176. package/dist/i18n/Trans.js +0 -24
  177. package/dist/i18n/context.js +0 -58
  178. package/dist/i18n/detect.js +0 -26
  179. package/dist/i18n/i18n.js +0 -49
  180. package/dist/i18n/index.js +0 -44
  181. package/dist/i18n/locales/en.js +0 -1
  182. package/dist/i18n/locales/zh-Hans.js +0 -1
  183. package/dist/i18n/locales/zh-Hant.js +0 -1
  184. package/dist/i18n/locales.js +0 -13
  185. package/dist/i18n/middleware.js +0 -30
  186. package/dist/lib/avatar-upload.js +0 -134
  187. package/dist/lib/config.js +0 -143
  188. package/dist/lib/constants.js +0 -50
  189. package/dist/lib/excerpt.js +0 -76
  190. package/dist/lib/favicon.js +0 -102
  191. package/dist/lib/feed.js +0 -123
  192. package/dist/lib/image-processor.js +0 -187
  193. package/dist/lib/image.js +0 -97
  194. package/dist/lib/index.js +0 -7
  195. package/dist/lib/markdown.js +0 -83
  196. package/dist/lib/media-helpers.js +0 -49
  197. package/dist/lib/media-upload.js +0 -104
  198. package/dist/lib/nav-reorder.js +0 -27
  199. package/dist/lib/navigation.js +0 -79
  200. package/dist/lib/pagination.js +0 -44
  201. package/dist/lib/render.js +0 -53
  202. package/dist/lib/schemas.js +0 -174
  203. package/dist/lib/sqid.js +0 -72
  204. package/dist/lib/sse.js +0 -218
  205. package/dist/lib/storage.js +0 -164
  206. package/dist/lib/theme.js +0 -65
  207. package/dist/lib/time.js +0 -159
  208. package/dist/lib/timeline.js +0 -95
  209. package/dist/lib/timezones.js +0 -388
  210. package/dist/lib/url.js +0 -89
  211. package/dist/lib/view.js +0 -217
  212. package/dist/middleware/auth.js +0 -52
  213. package/dist/middleware/onboarding.js +0 -41
  214. package/dist/routes/api/collections.js +0 -124
  215. package/dist/routes/api/nav-items.js +0 -104
  216. package/dist/routes/api/pages.js +0 -91
  217. package/dist/routes/api/posts.js +0 -218
  218. package/dist/routes/api/search.js +0 -48
  219. package/dist/routes/api/settings.js +0 -68
  220. package/dist/routes/api/upload.js +0 -246
  221. package/dist/routes/auth/reset.js +0 -221
  222. package/dist/routes/auth/setup.js +0 -194
  223. package/dist/routes/auth/signin.js +0 -176
  224. package/dist/routes/compose.js +0 -48
  225. package/dist/routes/dash/collections.js +0 -115
  226. package/dist/routes/dash/index.js +0 -118
  227. package/dist/routes/dash/media.js +0 -106
  228. package/dist/routes/dash/pages.js +0 -294
  229. package/dist/routes/dash/posts.js +0 -244
  230. package/dist/routes/dash/redirects.js +0 -257
  231. package/dist/routes/dash/settings.js +0 -379
  232. package/dist/routes/feed/rss.js +0 -62
  233. package/dist/routes/feed/sitemap.js +0 -49
  234. package/dist/routes/pages/archive.js +0 -62
  235. package/dist/routes/pages/collection.js +0 -34
  236. package/dist/routes/pages/collections.js +0 -28
  237. package/dist/routes/pages/featured.js +0 -36
  238. package/dist/routes/pages/home.js +0 -64
  239. package/dist/routes/pages/latest.js +0 -45
  240. package/dist/routes/pages/page.js +0 -68
  241. package/dist/routes/pages/post.js +0 -44
  242. package/dist/routes/pages/search.js +0 -54
  243. package/dist/services/collection.js +0 -109
  244. package/dist/services/index.js +0 -24
  245. package/dist/services/media.js +0 -117
  246. package/dist/services/navigation.js +0 -91
  247. package/dist/services/page.js +0 -84
  248. package/dist/services/post.js +0 -229
  249. package/dist/services/redirect.js +0 -48
  250. package/dist/services/search.js +0 -67
  251. package/dist/services/settings.js +0 -68
  252. package/dist/types/bindings.js +0 -3
  253. package/dist/types/config.js +0 -147
  254. package/dist/types/constants.js +0 -27
  255. package/dist/types/entities.js +0 -3
  256. package/dist/types/lingui-react-macro.d.js +0 -9
  257. package/dist/types/operations.js +0 -3
  258. package/dist/types/props.js +0 -3
  259. package/dist/types/sortablejs.d.js +0 -5
  260. package/dist/types/views.js +0 -5
  261. package/dist/types.js +0 -11
  262. package/dist/ui/color-themes.js +0 -268
  263. package/dist/ui/compose/ComposeDialog.js +0 -467
  264. package/dist/ui/compose/ComposePrompt.js +0 -55
  265. package/dist/ui/dash/ActionButtons.js +0 -46
  266. package/dist/ui/dash/CrudPageHeader.js +0 -22
  267. package/dist/ui/dash/DangerZone.js +0 -36
  268. package/dist/ui/dash/FormatBadge.js +0 -27
  269. package/dist/ui/dash/ListItemRow.js +0 -21
  270. package/dist/ui/dash/PageForm.js +0 -195
  271. package/dist/ui/dash/PostForm.js +0 -395
  272. package/dist/ui/dash/PostList.js +0 -83
  273. package/dist/ui/dash/StatusBadge.js +0 -46
  274. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  275. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  276. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  277. package/dist/ui/dash/index.js +0 -10
  278. package/dist/ui/dash/media/MediaListContent.js +0 -166
  279. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  280. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  281. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  282. package/dist/ui/dash/settings/AccountContent.js +0 -209
  283. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  284. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  285. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  286. package/dist/ui/feed/LinkCard.js +0 -72
  287. package/dist/ui/feed/NoteCard.js +0 -58
  288. package/dist/ui/feed/QuoteCard.js +0 -63
  289. package/dist/ui/feed/ThreadPreview.js +0 -48
  290. package/dist/ui/feed/TimelineFeed.js +0 -41
  291. package/dist/ui/feed/TimelineItem.js +0 -27
  292. package/dist/ui/font-themes.js +0 -36
  293. package/dist/ui/layouts/BaseLayout.js +0 -153
  294. package/dist/ui/layouts/DashLayout.js +0 -141
  295. package/dist/ui/layouts/SiteLayout.js +0 -169
  296. package/dist/ui/pages/ArchivePage.js +0 -143
  297. package/dist/ui/pages/CollectionPage.js +0 -70
  298. package/dist/ui/pages/CollectionsPage.js +0 -76
  299. package/dist/ui/pages/FeaturedPage.js +0 -24
  300. package/dist/ui/pages/HomePage.js +0 -24
  301. package/dist/ui/pages/PostPage.js +0 -55
  302. package/dist/ui/pages/SearchPage.js +0 -122
  303. package/dist/ui/pages/SinglePage.js +0 -23
  304. package/dist/ui/shared/EmptyState.js +0 -27
  305. package/dist/ui/shared/MediaGallery.js +0 -35
  306. package/dist/ui/shared/Pagination.js +0 -195
  307. package/dist/ui/shared/ThreadView.js +0 -108
  308. package/dist/ui/shared/index.js +0 -5
  309. package/dist/vendor/datastar.js +0 -1606
  310. package/src/lib/__tests__/config.test.ts +0 -192
  311. package/src/lib/config.ts +0 -167
  312. package/src/routes/compose.ts +0 -63
  313. package/src/ui/dash/PostForm.tsx +0 -360
  314. package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.3.27",
3
+ "version": "0.3.29",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "exports": {
@@ -8,14 +8,13 @@
8
8
  "types": "./src/index.ts",
9
9
  "default": "./dist/index.js"
10
10
  },
11
- "./i18n": {
12
- "types": "./src/i18n/index.ts",
13
- "default": "./dist/i18n/index.js"
14
- },
15
- "./preset.css": "./src/preset.css",
16
- "./client": "./dist/client.js"
11
+ "./i18n": "./src/i18n/index.ts"
12
+ },
13
+ "bin": {
14
+ "jant-reset-password": "bin/reset-password.js"
17
15
  },
18
16
  "files": [
17
+ "bin",
19
18
  "dist",
20
19
  "src"
21
20
  ],
@@ -25,16 +24,18 @@
25
24
  "dependencies": {
26
25
  "@aws-sdk/client-s3": "^3.987.0",
27
26
  "@lingui/core": "^5.9.0",
27
+ "@lingui/react": "^5.9.0",
28
28
  "@tailwindcss/typography": "^0.5.19",
29
29
  "basecoat-css": "^0.3.10",
30
30
  "better-auth": "^1.4.18",
31
31
  "drizzle-orm": "^0.45.1",
32
- "hono": "^4.11.7",
32
+ "lit": "^3.3.2",
33
+ "lucide-static": "^0.574.0",
33
34
  "marked": "^17.0.1",
35
+ "pinyin-pro": "^3.28.0",
34
36
  "sortablejs": "^1.15.6",
35
37
  "sqids": "^0.3.0",
36
38
  "uuidv7": "^1.1.0",
37
- "vite-ssr-components": "^0.5.2",
38
39
  "zod": "^4.3.6"
39
40
  },
40
41
  "peerDependencies": {
@@ -42,22 +43,27 @@
42
43
  "tailwindcss": "^4.0.0"
43
44
  },
44
45
  "devDependencies": {
46
+ "@cloudflare/vite-plugin": "^1.22.1",
45
47
  "@cloudflare/workers-types": "^4.20260131.0",
46
48
  "@lingui/cli": "^5.9.0",
47
49
  "@lingui/conf": "^5.9.0",
48
50
  "@lingui/format-po": "^5.9.0",
49
51
  "@lingui/swc-plugin": "^5.10.1",
50
- "@swc/cli": "^0.6.0",
51
52
  "@swc/core": "^1.15.11",
52
53
  "@types/better-sqlite3": "^7.6.13",
53
54
  "@types/node": "^25.1.0",
54
55
  "better-sqlite3": "^12.6.2",
55
56
  "drizzle-kit": "^0.31.8",
56
57
  "glob": "^13.0.0",
58
+ "happy-dom": "^20.6.3",
59
+ "@tailwindcss/vite": "^4.1.18",
57
60
  "tailwindcss": "^4.1.18",
58
61
  "tsx": "^4.21.0",
59
62
  "typescript": "^5.9.3",
60
- "vitest": "^4.0.18"
63
+ "unplugin-swc": "^1.5.9",
64
+ "vite": "^7.3.1",
65
+ "vitest": "^4.0.18",
66
+ "wrangler": "^4.61.1"
61
67
  },
62
68
  "repository": {
63
69
  "type": "git",
@@ -83,14 +89,18 @@
83
89
  "node": ">=24.0.0"
84
90
  },
85
91
  "scripts": {
86
- "build": "pnpm build:lib",
87
- "build:lib": "swc src -d dist --strip-leading-paths --ignore '**/__tests__/**' && pnpm build:types",
88
- "build:types": "tsc -p tsconfig.build.json",
89
- "typecheck": "tsc --noEmit",
92
+ "build": "pnpm build:lib && pnpm build:client",
93
+ "build:client": "vite build --config vite.config.client.ts",
94
+ "build:lib": "vite build --config vite.config.worker.ts",
95
+ "typecheck": "tsc -b --noEmit",
90
96
  "db:generate": "drizzle-kit generate",
91
97
  "i18n:extract": "lingui extract",
92
98
  "i18n:compile": "lingui compile --typescript",
93
99
  "i18n:build": "pnpm i18n:extract && pnpm i18n:compile",
100
+ "dev": "pnpm db:migrate:local && vite dev",
101
+ "dev:debug": "pnpm db:migrate:local && vite dev --port 19019",
102
+ "db:migrate:local": "yes | wrangler d1 migrations apply DB --local",
103
+ "db:migrate:remote": "wrangler d1 migrations apply DB --remote",
94
104
  "test": "vitest run",
95
105
  "test:watch": "vitest",
96
106
  "test:coverage": "vitest run --coverage"
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { Hono } from "hono";
8
8
  import type { Bindings } from "../../types.js";
9
- import type { AppVariables } from "../../app.js";
9
+ import type { AppVariables } from "../../types/app-context.js";
10
10
  import { createTestDatabase } from "./db.js";
11
11
  import { createPostService } from "../../services/post.js";
12
12
  import { createPageService } from "../../services/page.js";
@@ -16,8 +16,12 @@ import { createMediaService } from "../../services/media.js";
16
16
  import { createCollectionService } from "../../services/collection.js";
17
17
  import { createSearchService } from "../../services/search.js";
18
18
  import { createNavItemService } from "../../services/navigation.js";
19
+ import { createAuthService } from "../../services/auth.js";
19
20
  import type { Database } from "../../db/index.js";
20
21
  import type BetterSqlite3 from "better-sqlite3";
22
+ import { errorHandler } from "../../middleware/error-handler.js";
23
+ import { createI18n } from "../../i18n/i18n.js";
24
+ import { resolveConfig } from "../../lib/resolve-config.js";
21
25
 
22
26
  type Env = { Bindings: Bindings; Variables: AppVariables };
23
27
 
@@ -40,19 +44,24 @@ export function createTestApp(options: TestAppOptions = {}) {
40
44
  // Create a mock D1 for search service
41
45
  const mockD1 = createMockD1(sqlite);
42
46
 
47
+ const settingsService = createSettingsService(db);
43
48
  const services = {
44
49
  posts: createPostService(db),
45
50
  pages: createPageService(db),
46
- settings: createSettingsService(db),
51
+ settings: settingsService,
47
52
  redirects: createRedirectService(db),
48
53
  media: createMediaService(db),
49
54
  collections: createCollectionService(db),
50
55
  search: createSearchService(mockD1),
51
56
  navItems: createNavItemService(db),
57
+ auth: createAuthService(db, settingsService),
52
58
  };
53
59
 
54
60
  const app = new Hono<Env>();
55
61
 
62
+ // Global error handler: maps DomainError → HTTP responses
63
+ app.onError(errorHandler);
64
+
56
65
  // Inject env bindings and services middleware
57
66
  app.use("*", async (c, next) => {
58
67
  // Provide mock env bindings so c.env.* works in route handlers
@@ -61,9 +70,16 @@ export function createTestApp(options: TestAppOptions = {}) {
61
70
  } as AppVariables["services"] extends never ? never : Bindings;
62
71
 
63
72
  c.set("services", services as AppVariables["services"]);
64
- c.set("config", {});
73
+ const allSettings = await services.settings.getAll();
74
+ c.set("allSettings", allSettings);
75
+ c.set("appConfig", resolveConfig(c.env, allSettings));
65
76
  c.set("storage", null);
66
77
 
78
+ // i18n (English default for tests)
79
+ const i18n = createI18n("en");
80
+ c.set("lang", "en");
81
+ c.set("i18n", i18n);
82
+
67
83
  if (options.authenticated) {
68
84
  // Mock auth that always returns a session
69
85
  c.set("auth", {
@@ -89,7 +89,51 @@ export function createTestDatabase(options?: { fts?: boolean }) {
89
89
  // Apply 0006: rename slug to path on posts
90
90
  applyMigration(sqlite, "0006_rename_slug_to_path.sql");
91
91
 
92
+ // Apply 0007: post_collections M:N junction table
93
+ const m7 = readFileSync(
94
+ resolve(MIGRATIONS_DIR, "0007_post_collections_m2m.sql"),
95
+ "utf-8",
96
+ );
97
+ for (const stmt of m7.split("--> statement-breakpoint")) {
98
+ const trimmed = stmt.trim();
99
+ if (!trimmed) continue;
100
+ // Skip FTS trigger statements if FTS not requested
101
+ const isFts = trimmed.includes("posts_fts");
102
+ if (!options?.fts && isFts) continue;
103
+ try {
104
+ sqlite.exec(trimmed);
105
+ } catch {
106
+ // Ignore DROP TRIGGER failures silently
107
+ if (!trimmed.startsWith("DROP TRIGGER")) {
108
+ throw new Error(`Migration 0007 failed: ${trimmed.slice(0, 100)}`);
109
+ }
110
+ }
111
+ }
112
+
113
+ // Apply 0008: collection_dividers table
114
+ applyMigration(sqlite, "0008_add_collection_dividers.sql");
115
+
116
+ // Apply 0009: drop show_divider column from collections
117
+ applyMigration(sqlite, "0009_drop_collection_show_divider.sql");
118
+
119
+ // Apply 0010: performance indexes
120
+ applyMigration(sqlite, "0010_add_performance_indexes.sql");
121
+
92
122
  const db = drizzle(sqlite, { schema });
93
123
 
124
+ // Polyfill D1 batch() for test compatibility.
125
+ // In production, D1 batch executes statements atomically in a single transaction.
126
+ // In tests, better-sqlite3 is synchronous and single-threaded so sequential
127
+ // execution is effectively atomic.
128
+ Object.defineProperty(db, "batch", {
129
+ value: async (queries: PromiseLike<unknown>[]) => {
130
+ const results = [];
131
+ for (const q of queries) {
132
+ results.push(await q);
133
+ }
134
+ return results;
135
+ },
136
+ });
137
+
94
138
  return { db, sqlite };
95
139
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Mock for @lingui/core/macro in the Vitest environment.
3
+ *
4
+ * The real module requires the Babel macro ecosystem which is not available
5
+ * under Vitest. This mock replicates the *post-transformation* API surface:
6
+ * `msg()` / `defineMessage()` return a MessageDescriptor whose `id` equals
7
+ * the source `message`. At runtime `i18n._()` falls back to `message` when
8
+ * the ID is not in the catalog, so English source strings propagate through.
9
+ */
10
+
11
+ import type { MessageDescriptor } from "@lingui/core";
12
+
13
+ type MacroInput =
14
+ | { id?: string; message: string; comment?: string; context?: string }
15
+ | TemplateStringsArray;
16
+
17
+ function toDescriptor(
18
+ input: MacroInput,
19
+ ...args: unknown[]
20
+ ): MessageDescriptor {
21
+ if (typeof input === "object" && "message" in input) {
22
+ return { id: input.message, message: input.message } as MessageDescriptor;
23
+ }
24
+ // Template literal form: msg`some text`
25
+ const raw = (input as TemplateStringsArray).reduce(
26
+ (acc, str, i) => acc + str + (args[i] ?? ""),
27
+ "",
28
+ );
29
+ return { id: raw, message: raw } as MessageDescriptor;
30
+ }
31
+
32
+ export const msg = toDescriptor;
33
+ export const defineMessage = toDescriptor;
package/src/app.tsx CHANGED
@@ -4,11 +4,10 @@
4
4
 
5
5
  import { Hono } from "hono";
6
6
  import { createDatabase } from "./db/index.js";
7
- import { createServices, type Services } from "./services/index.js";
8
- import { createAuth, type Auth } from "./auth.js";
7
+ import { createServices } from "./services/index.js";
8
+ import { createAuth } from "./auth.js";
9
9
  import { i18nMiddleware } from "./i18n/index.js";
10
- import type { Bindings, JantConfig } from "./types.js";
11
- import { SETTINGS_KEYS } from "./lib/constants.js";
10
+ import type { Bindings } from "./types.js";
12
11
 
13
12
  // Routes - Auth
14
13
  import { setupRoutes } from "./routes/auth/setup.js";
@@ -34,6 +33,7 @@ import { mediaRoutes as dashMediaRoutes } from "./routes/dash/media.js";
34
33
  import { settingsRoutes as dashSettingsRoutes } from "./routes/dash/settings.js";
35
34
  import { redirectsRoutes as dashRedirectsRoutes } from "./routes/dash/redirects.js";
36
35
  import { collectionsRoutes as dashCollectionsRoutes } from "./routes/dash/collections.js";
36
+ import { appearanceRoutes as dashAppearanceRoutes } from "./routes/dash/appearance.js";
37
37
 
38
38
  // Routes - API
39
39
  import { postsApiRoutes } from "./routes/api/posts.js";
@@ -53,152 +53,148 @@ import { sitemapRoutes } from "./routes/feed/sitemap.js";
53
53
  // Middleware
54
54
  import { requireAuth } from "./middleware/auth.js";
55
55
  import { requireOnboarding } from "./middleware/onboarding.js";
56
+ import { errorHandler } from "./middleware/error-handler.js";
57
+ import { withConfig } from "./middleware/config.js";
56
58
 
57
- import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
58
- import { createStorageDriver, type StorageDriver } from "./lib/storage.js";
59
- import { BUILTIN_FONT_THEMES } from "./ui/font-themes.js";
60
- import { getMediaUrl, getPublicUrlForProvider } from "./lib/image.js";
59
+ import { createStorageDriver } from "./lib/storage.js";
61
60
  import { base64ToUint8Array } from "./lib/favicon.js";
61
+ import { type AppVariables, type App } from "./types/app-context.js";
62
62
 
63
- // Extend Hono's context variables
64
- export interface AppVariables {
65
- services: Services;
66
- auth: Auth;
67
- config: JantConfig;
68
- themeStyle: string;
69
- customCSS: string;
70
- isAuthenticated: boolean;
71
- storage: StorageDriver | null;
72
- faviconUrl?: string;
73
- noindex?: boolean;
74
- }
75
-
76
- export type App = Hono<{ Bindings: Bindings; Variables: AppVariables }>;
63
+ export type { AppVariables, App };
77
64
 
78
65
  /**
79
66
  * Create a Jant application
80
67
  *
81
- * @param config - Optional configuration
82
68
  * @returns Hono app instance
83
69
  *
84
- * Site settings (name, description, language) should be configured via
85
- * environment variables (SITE_NAME, SITE_DESCRIPTION, SITE_LANGUAGE).
86
- * They can also be set in the dashboard, which stores them in the database.
87
- *
88
70
  * @example
89
71
  * ```typescript
90
72
  * import { createApp } from "@jant/core";
91
73
  *
92
- * export default createApp({
93
- * cssVariables: { "--card-radius": "0" },
94
- * });
74
+ * export default createApp();
95
75
  * ```
96
76
  */
97
- export function createApp(config: JantConfig = {}): App {
98
- const resolvedConfig: JantConfig = { ...config };
99
-
77
+ export function createApp(): App {
100
78
  const app = new Hono<{ Bindings: Bindings; Variables: AppVariables }>();
101
79
 
102
- // Initialize services, auth, and config middleware
80
+ // Global error handler: maps DomainError → HTTP responses
81
+ app.onError(errorHandler);
82
+
83
+ // Lightweight init — no DB queries
103
84
  app.use("*", async (c, next) => {
85
+ if (!c.env.AUTH_SECRET) {
86
+ return c.html(
87
+ `<!DOCTYPE html>
88
+ <html lang="en">
89
+ <head>
90
+ <meta charset="utf-8">
91
+ <meta name="viewport" content="width=device-width, initial-scale=1">
92
+ <title>Configuration Error</title>
93
+ <style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#fafafa;color:#111}div{max-width:480px;text-align:center}h1{font-size:1.25rem;font-weight:600}p{color:#666;line-height:1.6}code{background:#eee;padding:2px 6px;border-radius:4px;font-size:.9em}</style>
94
+ </head>
95
+ <body>
96
+ <div>
97
+ <h1>AUTH_SECRET is not set</h1>
98
+ <p>Set <code>AUTH_SECRET</code> in <code>.dev.vars</code> or <code>wrangler.toml</code> to start Jant.</p>
99
+ </div>
100
+ </body>
101
+ </html>`,
102
+ 500,
103
+ );
104
+ }
105
+
104
106
  // Use withSession() to enable D1 Read Replication
105
107
  const session = c.env.DB.withSession();
106
108
 
107
109
  // Note: Drizzle ORM doesn't officially support D1DatabaseSession yet (issue #2226)
108
110
  // but it works at runtime. We use type assertion as a temporary workaround.
109
111
  const db = createDatabase(session as unknown as D1Database);
110
- const services = createServices(db, session as unknown as D1Database);
111
- c.set("services", services);
112
- c.set("config", resolvedConfig);
112
+ c.set("services", createServices(db, session as unknown as D1Database));
113
113
  c.set("storage", createStorageDriver(c.env));
114
114
 
115
- if (!c.env.AUTH_SECRET) {
116
- // eslint-disable-next-line no-console -- Startup warning is intentional
117
- console.warn(
118
- "[Jant] AUTH_SECRET is not set. Authentication is disabled. Set AUTH_SECRET in .dev.vars or wrangler.toml to enable auth.",
119
- );
120
- }
121
-
122
- if (c.env.AUTH_SECRET) {
123
- const baseURL = c.env.SITE_URL || new URL(c.req.url).origin;
124
- const requestUrl = new URL(c.req.url);
125
- const auth = createAuth(session as unknown as D1Database, {
115
+ const baseURL = c.env.SITE_URL || new URL(c.req.url).origin;
116
+ const requestUrl = new URL(c.req.url);
117
+ c.set(
118
+ "auth",
119
+ createAuth(session as unknown as D1Database, {
126
120
  secret: c.env.AUTH_SECRET,
127
121
  baseURL,
128
122
  useSecureCookies: requestUrl.protocol === "https:",
129
- });
130
- c.set("auth", auth);
131
- }
123
+ }),
124
+ );
132
125
 
133
126
  await next();
134
127
  });
135
128
 
136
- // Onboarding gate redirect to /setup if not yet initialized
137
- app.use("*", requireOnboarding());
129
+ // --- Routes that don't need config/theme ---
138
130
 
139
- // Theme middleware - resolve active color theme, font theme, custom CSS, and auth state
140
- app.use("*", async (c, next) => {
141
- const [themeId, fontThemeId, customCSS, noindexValue, avatarKey] =
142
- await Promise.all([
143
- c.var.services.settings.get(SETTINGS_KEYS.THEME),
144
- c.var.services.settings.get("FONT_THEME"),
145
- c.var.services.settings.get(SETTINGS_KEYS.CUSTOM_CSS),
146
- c.var.services.settings.get("NOINDEX"),
147
- c.var.services.settings.get("SITE_AVATAR"),
148
- ]);
149
- const themes = getAvailableThemes(resolvedConfig);
150
- const activeTheme = themeId
151
- ? themes.find((t) => t.id === themeId)
152
- : undefined;
153
-
154
- // Build font override CSS variables
155
- const fontTheme = fontThemeId
156
- ? BUILTIN_FONT_THEMES.find((f) => f.id === fontThemeId)
157
- : undefined;
158
- const fontOverrides: Record<string, string> = {};
159
- if (fontTheme) {
160
- fontOverrides["--font-body"] = fontTheme.fontFamily;
161
- }
131
+ // Health check
132
+ app.get("/health", (c) => c.json({ status: "ok" }));
162
133
 
163
- const themeStyle = buildThemeStyle(activeTheme, {
164
- ...resolvedConfig.cssVariables,
165
- ...fontOverrides,
166
- });
167
- c.set("themeStyle", themeStyle);
168
- c.set("customCSS", customCSS ?? "");
169
-
170
- // Noindex
171
- c.set("noindex", noindexValue === "true");
172
-
173
- // Resolve favicon from avatar storage key
174
- if (avatarKey) {
175
- const publicUrl = getPublicUrlForProvider(
176
- c.env.STORAGE_DRIVER || "r2",
177
- c.env.R2_PUBLIC_URL,
178
- c.env.S3_PUBLIC_URL,
179
- );
180
- c.set("faviconUrl", getMediaUrl(avatarKey, publicUrl));
134
+ // Media files from storage (path matches storage key: media/YYYY/MM/uuid.ext)
135
+ app.get("/media/*", async (c) => {
136
+ const storage = c.var.storage;
137
+ if (!storage) {
138
+ return c.notFound();
181
139
  }
182
140
 
183
- // Check auth state for data-authenticated attribute on <body>
184
- let isAuthenticated = false;
185
- if (c.var.auth) {
186
- try {
187
- const session = await c.var.auth.api.getSession({
188
- headers: c.req.raw.headers,
189
- });
190
- isAuthenticated = !!session;
191
- } catch {
192
- // Not authenticated
193
- }
141
+ // The storage key is the full path without the leading "/"
142
+ const storageKey = c.req.path.slice(1);
143
+ const object = await storage.get(storageKey);
144
+ if (!object) {
145
+ return c.notFound();
194
146
  }
195
- c.set("isAuthenticated", isAuthenticated);
196
147
 
197
- await next();
148
+ const headers = new Headers();
149
+ headers.set(
150
+ "Content-Type",
151
+ object.contentType || "application/octet-stream",
152
+ );
153
+ headers.set("Cache-Control", "public, max-age=31536000, immutable");
154
+
155
+ return new Response(object.body, { headers });
198
156
  });
199
157
 
200
- // i18n middleware
201
- app.use("*", i18nMiddleware());
158
+ // better-auth handler
159
+ app.all("/api/auth/*", async (c) => {
160
+ return c.var.auth.handler(c.req.raw);
161
+ });
162
+
163
+ // Favicon routes - serve from DB settings (small files, avoids R2 round-trip)
164
+ app.get("/favicon.ico", async (c) => {
165
+ const data = await c.var.services.settings.get("SITE_FAVICON_ICO");
166
+ if (!data) return c.notFound();
167
+
168
+ return new Response(base64ToUint8Array(data), {
169
+ headers: {
170
+ "Content-Type": "image/x-icon",
171
+ "Cache-Control": "public, max-age=86400",
172
+ },
173
+ });
174
+ });
175
+
176
+ app.get("/apple-touch-icon.png", async (c) => {
177
+ const storage = c.var.storage;
178
+ const storageKey = await c.var.services.settings.get(
179
+ "SITE_FAVICON_APPLE_TOUCH",
180
+ );
181
+ if (!storage || !storageKey) return c.notFound();
182
+
183
+ const object = await storage.get(storageKey);
184
+ if (!object) return c.notFound();
185
+
186
+ return new Response(object.body, {
187
+ headers: {
188
+ "Content-Type": "image/png",
189
+ "Cache-Control": "public, max-age=86400",
190
+ },
191
+ });
192
+ });
193
+
194
+ // --- Middleware for all remaining routes ---
195
+
196
+ // Onboarding gate — redirect to /setup if not yet initialized
197
+ app.use("*", requireOnboarding());
202
198
 
203
199
  // Trailing slash redirect (redirect /foo/ to /foo)
204
200
  app.use("*", async (c, next) => {
@@ -226,47 +222,11 @@ export function createApp(config: JantConfig = {}): App {
226
222
  await next();
227
223
  });
228
224
 
229
- // Health check
230
- app.get("/health", (c) =>
231
- c.json({
232
- status: "ok",
233
- auth: c.env.AUTH_SECRET ? "configured" : "missing",
234
- authSecretLength: c.env.AUTH_SECRET?.length ?? 0,
235
- }),
236
- );
237
-
238
- // Favicon routes - serve from DB settings (small files, avoids R2 round-trip)
239
- app.get("/favicon.ico", async (c) => {
240
- const data = await c.var.services.settings.get("SITE_FAVICON_ICO");
241
- if (!data) return c.notFound();
242
-
243
- return new Response(base64ToUint8Array(data), {
244
- headers: {
245
- "Content-Type": "image/x-icon",
246
- "Cache-Control": "public, max-age=86400",
247
- },
248
- });
249
- });
250
-
251
- app.get("/apple-touch-icon.png", async (c) => {
252
- const data = await c.var.services.settings.get("SITE_FAVICON_APPLE_TOUCH");
253
- if (!data) return c.notFound();
254
-
255
- return new Response(base64ToUint8Array(data), {
256
- headers: {
257
- "Content-Type": "image/png",
258
- "Cache-Control": "public, max-age=86400",
259
- },
260
- });
261
- });
225
+ // Config + i18n — loads settings, resolves config/theme
226
+ app.use("*", withConfig());
227
+ app.use("*", i18nMiddleware());
262
228
 
263
- // better-auth handler
264
- app.all("/api/auth/*", async (c) => {
265
- if (!c.var.auth) {
266
- return c.json({ error: "Auth not configured. Set AUTH_SECRET." }, 500);
267
- }
268
- return c.var.auth.handler(c.req.raw);
269
- });
229
+ // --- Routes that need config ---
270
230
 
271
231
  // API Routes
272
232
  app.route("/api/posts", postsApiRoutes);
@@ -287,36 +247,13 @@ export function createApp(config: JantConfig = {}): App {
287
247
  app.route("/dash/pages", dashPagesRoutes);
288
248
  app.route("/dash/media", dashMediaRoutes);
289
249
  app.route("/dash/settings", dashSettingsRoutes);
290
- app.route("/dash/redirects", dashRedirectsRoutes);
250
+ app.route("/dash/appearance", dashAppearanceRoutes);
251
+ app.route("/dash/settings/redirects", dashRedirectsRoutes);
291
252
  app.route("/dash/collections", dashCollectionsRoutes);
292
253
  // API routes
293
254
  app.route("/api/upload", uploadApiRoutes);
294
255
  app.route("/api/search", searchApiRoutes);
295
256
 
296
- // Media files from storage (path matches storage key: media/YYYY/MM/uuid.ext)
297
- app.get("/media/*", async (c) => {
298
- const storage = c.var.storage;
299
- if (!storage) {
300
- return c.notFound();
301
- }
302
-
303
- // The storage key is the full path without the leading "/"
304
- const storageKey = c.req.path.slice(1);
305
- const object = await storage.get(storageKey);
306
- if (!object) {
307
- return c.notFound();
308
- }
309
-
310
- const headers = new Headers();
311
- headers.set(
312
- "Content-Type",
313
- object.contentType || "application/octet-stream",
314
- );
315
- headers.set("Cache-Control", "public, max-age=31536000, immutable");
316
-
317
- return new Response(object.body, { headers });
318
- });
319
-
320
257
  // Compose route (auth enforced in route middleware)
321
258
  app.route("/compose", composeRoutes);
322
259
 
package/src/client.ts CHANGED
@@ -13,3 +13,16 @@ import "./lib/image-processor.js";
13
13
  import "./lib/media-upload.js";
14
14
  import "./lib/avatar-upload.js";
15
15
  import "./lib/nav-reorder.js";
16
+ import "./lib/collections-reorder.js";
17
+
18
+ // Lit Web Components
19
+ import "./ui/components/jant-compose-dialog.js";
20
+ import "./ui/components/jant-compose-editor.js";
21
+ import "./lib/compose-bridge.js";
22
+ import "./ui/components/jant-settings-general.js";
23
+ import "./ui/components/jant-settings-avatar.js";
24
+ import "./lib/settings-bridge.js";
25
+ import "./ui/components/jant-collection-form.js";
26
+ import "./lib/collection-form-bridge.js";
27
+ import "./ui/components/jant-post-form.js";
28
+ import "./lib/post-form-bridge.js";