@jant/core 0.3.27 → 0.3.28

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 (313) hide show
  1. package/dist/client/client.css +1 -0
  2. package/dist/client/client.js +31561 -0
  3. package/dist/index.js +15209 -15
  4. package/package.json +21 -15
  5. package/src/__tests__/helpers/app.ts +19 -3
  6. package/src/__tests__/helpers/db.ts +44 -0
  7. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  8. package/src/app.tsx +111 -174
  9. package/src/client.ts +13 -0
  10. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  11. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  12. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  13. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  14. package/src/db/schema.ts +24 -4
  15. package/src/i18n/locales/en.po +810 -385
  16. package/src/i18n/locales/en.ts +1 -1
  17. package/src/i18n/locales/zh-Hans.po +733 -522
  18. package/src/i18n/locales/zh-Hans.ts +1 -1
  19. package/src/i18n/locales/zh-Hant.po +733 -522
  20. package/src/i18n/locales/zh-Hant.ts +1 -1
  21. package/src/i18n/middleware.ts +7 -11
  22. package/src/index.ts +1 -1
  23. package/src/lib/__tests__/icons.test.ts +178 -0
  24. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  25. package/src/lib/__tests__/schemas.test.ts +12 -6
  26. package/src/lib/__tests__/theme.test.ts +62 -0
  27. package/src/lib/__tests__/timezones.test.ts +1 -1
  28. package/src/lib/__tests__/url.test.ts +12 -0
  29. package/src/lib/__tests__/view.test.ts +1 -5
  30. package/src/lib/avatar-upload.ts +18 -10
  31. package/src/lib/collection-form-bridge.ts +52 -0
  32. package/src/lib/collections-reorder.ts +28 -0
  33. package/src/lib/compose-bridge.ts +251 -0
  34. package/src/lib/errors.ts +116 -0
  35. package/src/lib/excerpt.ts +1 -1
  36. package/src/lib/favicon.ts +3 -5
  37. package/src/lib/html.ts +22 -0
  38. package/src/lib/icon-catalog.ts +181 -0
  39. package/src/lib/icons.ts +202 -0
  40. package/src/lib/navigation.ts +18 -33
  41. package/src/lib/pagination.ts +3 -2
  42. package/src/lib/post-form-bridge.ts +136 -0
  43. package/src/lib/render.tsx +11 -4
  44. package/src/lib/resolve-config.ts +157 -0
  45. package/src/lib/schemas.ts +76 -12
  46. package/src/lib/settings-bridge.ts +139 -0
  47. package/src/lib/storage.ts +37 -16
  48. package/src/lib/theme.ts +5 -7
  49. package/src/lib/timeline.ts +4 -8
  50. package/src/lib/toast.ts +134 -0
  51. package/src/lib/upload.ts +71 -0
  52. package/src/lib/url.ts +9 -1
  53. package/src/lib/version.ts +16 -0
  54. package/src/lib/view.ts +9 -10
  55. package/src/middleware/__tests__/auth.test.ts +6 -28
  56. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  57. package/src/middleware/auth.ts +6 -12
  58. package/src/middleware/config.ts +51 -0
  59. package/src/middleware/error-handler.ts +56 -0
  60. package/src/middleware/onboarding.ts +1 -1
  61. package/src/preset.css +6 -0
  62. package/src/routes/__tests__/compose.test.ts +104 -17
  63. package/src/routes/api/__tests__/collections.test.ts +93 -2
  64. package/src/routes/api/__tests__/posts.test.ts +2 -1
  65. package/src/routes/api/__tests__/settings.test.ts +1 -1
  66. package/src/routes/api/collections.ts +64 -68
  67. package/src/routes/api/nav-items.ts +21 -59
  68. package/src/routes/api/pages.ts +18 -46
  69. package/src/routes/api/posts.ts +64 -86
  70. package/src/routes/api/search.ts +6 -4
  71. package/src/routes/api/settings.ts +8 -24
  72. package/src/routes/api/upload.ts +55 -53
  73. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  74. package/src/routes/auth/reset.tsx +17 -66
  75. package/src/routes/auth/setup.tsx +67 -11
  76. package/src/routes/auth/signin.tsx +44 -8
  77. package/src/routes/compose.tsx +194 -0
  78. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  79. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  80. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  81. package/src/routes/dash/appearance.tsx +173 -0
  82. package/src/routes/dash/collections.tsx +80 -14
  83. package/src/routes/dash/index.tsx +12 -14
  84. package/src/routes/dash/media.tsx +46 -49
  85. package/src/routes/dash/pages.tsx +85 -37
  86. package/src/routes/dash/posts.tsx +60 -23
  87. package/src/routes/dash/redirects.tsx +43 -33
  88. package/src/routes/dash/settings.tsx +234 -214
  89. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  90. package/src/routes/feed/rss.ts +11 -16
  91. package/src/routes/feed/sitemap.ts +15 -9
  92. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  93. package/src/routes/pages/archive.tsx +2 -2
  94. package/src/routes/pages/collection.tsx +76 -9
  95. package/src/routes/pages/collections.tsx +3 -1
  96. package/src/routes/pages/featured.tsx +2 -2
  97. package/src/routes/pages/home.tsx +3 -3
  98. package/src/routes/pages/latest.tsx +2 -2
  99. package/src/routes/pages/page.tsx +2 -2
  100. package/src/routes/pages/post.tsx +2 -2
  101. package/src/routes/pages/search.tsx +2 -2
  102. package/src/services/__tests__/collection.test.ts +324 -34
  103. package/src/services/__tests__/media.test.ts +1 -1
  104. package/src/services/__tests__/page.test.ts +116 -1
  105. package/src/services/auth.ts +88 -0
  106. package/src/services/collection.ts +169 -30
  107. package/src/services/index.ts +8 -3
  108. package/src/services/media.ts +39 -12
  109. package/src/services/navigation.ts +17 -5
  110. package/src/services/page.ts +24 -4
  111. package/src/services/post.ts +87 -19
  112. package/src/services/search.ts +0 -1
  113. package/src/services/settings.ts +21 -13
  114. package/src/style.css +3 -0
  115. package/src/styles/components.css +42 -1
  116. package/src/styles/tokens.css +4 -0
  117. package/src/styles/ui.css +902 -73
  118. package/src/types/app-context.ts +25 -0
  119. package/src/types/bindings.ts +1 -0
  120. package/src/types/config.ts +60 -23
  121. package/src/types/entities.ts +12 -2
  122. package/src/types/lingui-react-macro.d.ts +3 -3
  123. package/src/types/operations.ts +2 -4
  124. package/src/types/views.ts +1 -3
  125. package/src/ui/__tests__/font-themes.test.ts +27 -8
  126. package/src/ui/color-themes.ts +1 -1
  127. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  128. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  129. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  130. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  131. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  132. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  133. package/src/ui/components/collection-types.ts +45 -0
  134. package/src/ui/components/compose-types.ts +75 -0
  135. package/src/ui/components/jant-collection-form.ts +512 -0
  136. package/src/ui/components/jant-compose-dialog.ts +494 -0
  137. package/src/ui/components/jant-compose-editor.ts +799 -0
  138. package/src/ui/components/jant-post-form.ts +290 -0
  139. package/src/ui/components/jant-settings-avatar.ts +231 -0
  140. package/src/ui/components/jant-settings-general.ts +436 -0
  141. package/src/ui/components/post-form-template.ts +260 -0
  142. package/src/ui/components/post-form-types.ts +87 -0
  143. package/src/ui/components/settings-types.ts +62 -0
  144. package/src/ui/compose/ComposeDialog.tsx +141 -385
  145. package/src/ui/compose/ComposePrompt.tsx +3 -3
  146. package/src/ui/dash/PostList.tsx +55 -61
  147. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  148. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  149. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  150. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  151. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  152. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  153. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  154. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  155. package/src/ui/dash/index.ts +1 -1
  156. package/src/ui/dash/posts/PostForm.tsx +248 -0
  157. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  158. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  159. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  160. package/src/ui/font-themes.ts +115 -32
  161. package/src/ui/layouts/BaseLayout.tsx +49 -19
  162. package/src/ui/layouts/DashLayout.tsx +14 -9
  163. package/src/ui/layouts/SiteLayout.tsx +38 -23
  164. package/src/ui/pages/CollectionPage.tsx +12 -2
  165. package/src/ui/pages/CollectionsPage.tsx +27 -27
  166. package/src/ui/pages/HomePage.tsx +15 -6
  167. package/src/ui/pages/SearchPage.tsx +1 -2
  168. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  169. package/src/ui/shared/Pagination.tsx +2 -2
  170. package/dist/app.js +0 -267
  171. package/dist/auth.js +0 -39
  172. package/dist/client.js +0 -13
  173. package/dist/db/index.js +0 -10
  174. package/dist/db/schema.js +0 -224
  175. package/dist/i18n/Trans.js +0 -24
  176. package/dist/i18n/context.js +0 -58
  177. package/dist/i18n/detect.js +0 -26
  178. package/dist/i18n/i18n.js +0 -49
  179. package/dist/i18n/index.js +0 -44
  180. package/dist/i18n/locales/en.js +0 -1
  181. package/dist/i18n/locales/zh-Hans.js +0 -1
  182. package/dist/i18n/locales/zh-Hant.js +0 -1
  183. package/dist/i18n/locales.js +0 -13
  184. package/dist/i18n/middleware.js +0 -30
  185. package/dist/lib/avatar-upload.js +0 -134
  186. package/dist/lib/config.js +0 -143
  187. package/dist/lib/constants.js +0 -50
  188. package/dist/lib/excerpt.js +0 -76
  189. package/dist/lib/favicon.js +0 -102
  190. package/dist/lib/feed.js +0 -123
  191. package/dist/lib/image-processor.js +0 -187
  192. package/dist/lib/image.js +0 -97
  193. package/dist/lib/index.js +0 -7
  194. package/dist/lib/markdown.js +0 -83
  195. package/dist/lib/media-helpers.js +0 -49
  196. package/dist/lib/media-upload.js +0 -104
  197. package/dist/lib/nav-reorder.js +0 -27
  198. package/dist/lib/navigation.js +0 -79
  199. package/dist/lib/pagination.js +0 -44
  200. package/dist/lib/render.js +0 -53
  201. package/dist/lib/schemas.js +0 -174
  202. package/dist/lib/sqid.js +0 -72
  203. package/dist/lib/sse.js +0 -218
  204. package/dist/lib/storage.js +0 -164
  205. package/dist/lib/theme.js +0 -65
  206. package/dist/lib/time.js +0 -159
  207. package/dist/lib/timeline.js +0 -95
  208. package/dist/lib/timezones.js +0 -388
  209. package/dist/lib/url.js +0 -89
  210. package/dist/lib/view.js +0 -217
  211. package/dist/middleware/auth.js +0 -52
  212. package/dist/middleware/onboarding.js +0 -41
  213. package/dist/routes/api/collections.js +0 -124
  214. package/dist/routes/api/nav-items.js +0 -104
  215. package/dist/routes/api/pages.js +0 -91
  216. package/dist/routes/api/posts.js +0 -218
  217. package/dist/routes/api/search.js +0 -48
  218. package/dist/routes/api/settings.js +0 -68
  219. package/dist/routes/api/upload.js +0 -246
  220. package/dist/routes/auth/reset.js +0 -221
  221. package/dist/routes/auth/setup.js +0 -194
  222. package/dist/routes/auth/signin.js +0 -176
  223. package/dist/routes/compose.js +0 -48
  224. package/dist/routes/dash/collections.js +0 -115
  225. package/dist/routes/dash/index.js +0 -118
  226. package/dist/routes/dash/media.js +0 -106
  227. package/dist/routes/dash/pages.js +0 -294
  228. package/dist/routes/dash/posts.js +0 -244
  229. package/dist/routes/dash/redirects.js +0 -257
  230. package/dist/routes/dash/settings.js +0 -379
  231. package/dist/routes/feed/rss.js +0 -62
  232. package/dist/routes/feed/sitemap.js +0 -49
  233. package/dist/routes/pages/archive.js +0 -62
  234. package/dist/routes/pages/collection.js +0 -34
  235. package/dist/routes/pages/collections.js +0 -28
  236. package/dist/routes/pages/featured.js +0 -36
  237. package/dist/routes/pages/home.js +0 -64
  238. package/dist/routes/pages/latest.js +0 -45
  239. package/dist/routes/pages/page.js +0 -68
  240. package/dist/routes/pages/post.js +0 -44
  241. package/dist/routes/pages/search.js +0 -54
  242. package/dist/services/collection.js +0 -109
  243. package/dist/services/index.js +0 -24
  244. package/dist/services/media.js +0 -117
  245. package/dist/services/navigation.js +0 -91
  246. package/dist/services/page.js +0 -84
  247. package/dist/services/post.js +0 -229
  248. package/dist/services/redirect.js +0 -48
  249. package/dist/services/search.js +0 -67
  250. package/dist/services/settings.js +0 -68
  251. package/dist/types/bindings.js +0 -3
  252. package/dist/types/config.js +0 -147
  253. package/dist/types/constants.js +0 -27
  254. package/dist/types/entities.js +0 -3
  255. package/dist/types/lingui-react-macro.d.js +0 -9
  256. package/dist/types/operations.js +0 -3
  257. package/dist/types/props.js +0 -3
  258. package/dist/types/sortablejs.d.js +0 -5
  259. package/dist/types/views.js +0 -5
  260. package/dist/types.js +0 -11
  261. package/dist/ui/color-themes.js +0 -268
  262. package/dist/ui/compose/ComposeDialog.js +0 -467
  263. package/dist/ui/compose/ComposePrompt.js +0 -55
  264. package/dist/ui/dash/ActionButtons.js +0 -46
  265. package/dist/ui/dash/CrudPageHeader.js +0 -22
  266. package/dist/ui/dash/DangerZone.js +0 -36
  267. package/dist/ui/dash/FormatBadge.js +0 -27
  268. package/dist/ui/dash/ListItemRow.js +0 -21
  269. package/dist/ui/dash/PageForm.js +0 -195
  270. package/dist/ui/dash/PostForm.js +0 -395
  271. package/dist/ui/dash/PostList.js +0 -83
  272. package/dist/ui/dash/StatusBadge.js +0 -46
  273. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  274. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  275. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  276. package/dist/ui/dash/index.js +0 -10
  277. package/dist/ui/dash/media/MediaListContent.js +0 -166
  278. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  279. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  280. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  281. package/dist/ui/dash/settings/AccountContent.js +0 -209
  282. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  283. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  284. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  285. package/dist/ui/feed/LinkCard.js +0 -72
  286. package/dist/ui/feed/NoteCard.js +0 -58
  287. package/dist/ui/feed/QuoteCard.js +0 -63
  288. package/dist/ui/feed/ThreadPreview.js +0 -48
  289. package/dist/ui/feed/TimelineFeed.js +0 -41
  290. package/dist/ui/feed/TimelineItem.js +0 -27
  291. package/dist/ui/font-themes.js +0 -36
  292. package/dist/ui/layouts/BaseLayout.js +0 -153
  293. package/dist/ui/layouts/DashLayout.js +0 -141
  294. package/dist/ui/layouts/SiteLayout.js +0 -169
  295. package/dist/ui/pages/ArchivePage.js +0 -143
  296. package/dist/ui/pages/CollectionPage.js +0 -70
  297. package/dist/ui/pages/CollectionsPage.js +0 -76
  298. package/dist/ui/pages/FeaturedPage.js +0 -24
  299. package/dist/ui/pages/HomePage.js +0 -24
  300. package/dist/ui/pages/PostPage.js +0 -55
  301. package/dist/ui/pages/SearchPage.js +0 -122
  302. package/dist/ui/pages/SinglePage.js +0 -23
  303. package/dist/ui/shared/EmptyState.js +0 -27
  304. package/dist/ui/shared/MediaGallery.js +0 -35
  305. package/dist/ui/shared/Pagination.js +0 -195
  306. package/dist/ui/shared/ThreadView.js +0 -108
  307. package/dist/ui/shared/index.js +0 -5
  308. package/dist/vendor/datastar.js +0 -1606
  309. package/src/lib/__tests__/config.test.ts +0 -192
  310. package/src/lib/config.ts +0 -167
  311. package/src/routes/compose.ts +0 -63
  312. package/src/ui/dash/PostForm.tsx +0 -360
  313. 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.28",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "exports": {
@@ -8,12 +8,7 @@
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"
17
12
  },
18
13
  "files": [
19
14
  "dist",
@@ -25,16 +20,18 @@
25
20
  "dependencies": {
26
21
  "@aws-sdk/client-s3": "^3.987.0",
27
22
  "@lingui/core": "^5.9.0",
23
+ "@lingui/react": "^5.9.0",
28
24
  "@tailwindcss/typography": "^0.5.19",
29
25
  "basecoat-css": "^0.3.10",
30
26
  "better-auth": "^1.4.18",
31
27
  "drizzle-orm": "^0.45.1",
32
- "hono": "^4.11.7",
28
+ "lit": "^3.3.2",
29
+ "lucide-static": "^0.574.0",
33
30
  "marked": "^17.0.1",
31
+ "pinyin-pro": "^3.28.0",
34
32
  "sortablejs": "^1.15.6",
35
33
  "sqids": "^0.3.0",
36
34
  "uuidv7": "^1.1.0",
37
- "vite-ssr-components": "^0.5.2",
38
35
  "zod": "^4.3.6"
39
36
  },
40
37
  "peerDependencies": {
@@ -42,22 +39,27 @@
42
39
  "tailwindcss": "^4.0.0"
43
40
  },
44
41
  "devDependencies": {
42
+ "@cloudflare/vite-plugin": "^1.22.1",
45
43
  "@cloudflare/workers-types": "^4.20260131.0",
46
44
  "@lingui/cli": "^5.9.0",
47
45
  "@lingui/conf": "^5.9.0",
48
46
  "@lingui/format-po": "^5.9.0",
49
47
  "@lingui/swc-plugin": "^5.10.1",
50
- "@swc/cli": "^0.6.0",
51
48
  "@swc/core": "^1.15.11",
52
49
  "@types/better-sqlite3": "^7.6.13",
53
50
  "@types/node": "^25.1.0",
54
51
  "better-sqlite3": "^12.6.2",
55
52
  "drizzle-kit": "^0.31.8",
56
53
  "glob": "^13.0.0",
54
+ "happy-dom": "^20.6.3",
55
+ "@tailwindcss/vite": "^4.1.18",
57
56
  "tailwindcss": "^4.1.18",
58
57
  "tsx": "^4.21.0",
59
58
  "typescript": "^5.9.3",
60
- "vitest": "^4.0.18"
59
+ "unplugin-swc": "^1.5.9",
60
+ "vite": "^7.3.1",
61
+ "vitest": "^4.0.18",
62
+ "wrangler": "^4.61.1"
61
63
  },
62
64
  "repository": {
63
65
  "type": "git",
@@ -83,14 +85,18 @@
83
85
  "node": ">=24.0.0"
84
86
  },
85
87
  "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",
88
+ "build": "pnpm build:lib && pnpm build:client",
89
+ "build:client": "vite build --config vite.config.client.ts",
90
+ "build:lib": "vite build --config vite.config.worker.ts",
91
+ "typecheck": "tsc -b --noEmit",
90
92
  "db:generate": "drizzle-kit generate",
91
93
  "i18n:extract": "lingui extract",
92
94
  "i18n:compile": "lingui compile --typescript",
93
95
  "i18n:build": "pnpm i18n:extract && pnpm i18n:compile",
96
+ "dev": "pnpm db:migrate:local && vite dev",
97
+ "dev:debug": "pnpm db:migrate:local && vite dev --port 19019",
98
+ "db:migrate:local": "yes | wrangler d1 migrations apply DB --local",
99
+ "db:migrate:remote": "wrangler d1 migrations apply DB --remote",
94
100
  "test": "vitest run",
95
101
  "test:watch": "vitest",
96
102
  "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";