emdash 0.23.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/dist/api/route-utils.mjs +10 -10
  2. package/dist/api/schemas/index.d.mts +1 -1
  3. package/dist/{api-BQ_yKRdG.mjs → api-d_0wi9fW.mjs} +13 -13
  4. package/dist/{api-BQ_yKRdG.mjs.map → api-d_0wi9fW.mjs.map} +1 -1
  5. package/dist/{apply-CWRyxIoO.mjs → apply-BE9UaWX0.mjs} +16 -16
  6. package/dist/{apply-CWRyxIoO.mjs.map → apply-BE9UaWX0.mjs.map} +1 -1
  7. package/dist/astro/index.d.mts +2 -2
  8. package/dist/astro/index.mjs +25 -6
  9. package/dist/astro/index.mjs.map +1 -1
  10. package/dist/astro/middleware/auth.d.mts +2 -2
  11. package/dist/astro/middleware/auth.mjs +2 -2
  12. package/dist/astro/middleware/redirect.mjs +4 -4
  13. package/dist/astro/middleware.d.mts.map +1 -1
  14. package/dist/astro/middleware.mjs +46 -46
  15. package/dist/astro/middleware.mjs.map +1 -1
  16. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +3 -3
  17. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +3 -3
  18. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
  19. package/dist/astro/routes/api/admin/api-tokens/index.mjs +3 -3
  20. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +3 -3
  21. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +4 -4
  22. package/dist/astro/routes/api/admin/byline-fields/index.mjs +4 -4
  23. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +4 -4
  24. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +9 -9
  25. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +9 -9
  26. package/dist/astro/routes/api/admin/bylines/index.mjs +9 -9
  27. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +7 -7
  28. package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
  29. package/dist/astro/routes/api/admin/comments/bulk.mjs +6 -6
  30. package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
  31. package/dist/astro/routes/api/admin/comments/index.mjs +6 -6
  32. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  33. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  34. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
  35. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
  36. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +26 -26
  37. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +26 -26
  38. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +26 -26
  39. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +26 -26
  40. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +26 -26
  41. package/dist/astro/routes/api/admin/plugins/index.mjs +26 -26
  42. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  43. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +26 -26
  44. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +26 -26
  45. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +26 -26
  46. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +26 -26
  47. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +27 -27
  48. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +26 -26
  49. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +27 -27
  50. package/dist/astro/routes/api/admin/plugins/updates.mjs +26 -26
  51. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +26 -26
  52. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  53. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +26 -26
  54. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  55. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  56. package/dist/astro/routes/api/admin/users/_id_/index.mjs +3 -3
  57. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +2 -2
  58. package/dist/astro/routes/api/admin/users/index.mjs +3 -3
  59. package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
  60. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  61. package/dist/astro/routes/api/auth/invite/complete.mjs +3 -3
  62. package/dist/astro/routes/api/auth/invite/index.mjs +3 -3
  63. package/dist/astro/routes/api/auth/invite/register-options.mjs +3 -3
  64. package/dist/astro/routes/api/auth/logout.mjs +2 -2
  65. package/dist/astro/routes/api/auth/magic-link/send.mjs +4 -4
  66. package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
  67. package/dist/astro/routes/api/auth/me.mjs +4 -4
  68. package/dist/astro/routes/api/auth/passkey/_id_.mjs +3 -3
  69. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  70. package/dist/astro/routes/api/auth/passkey/options.mjs +4 -4
  71. package/dist/astro/routes/api/auth/passkey/register/options.mjs +3 -3
  72. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +3 -3
  73. package/dist/astro/routes/api/auth/passkey/verify.mjs +3 -3
  74. package/dist/astro/routes/api/auth/signup/complete.mjs +3 -3
  75. package/dist/astro/routes/api/auth/signup/request.mjs +4 -4
  76. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  77. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +6 -6
  78. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +6 -6
  79. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  80. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  81. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  82. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  83. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +4 -4
  84. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +4 -4
  85. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  86. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  87. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +4 -4
  88. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +8 -8
  89. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  90. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  91. package/dist/astro/routes/api/content/_collection_/_id_.mjs +4 -4
  92. package/dist/astro/routes/api/content/_collection_/authors.mjs +3 -3
  93. package/dist/astro/routes/api/content/_collection_/index.mjs +4 -4
  94. package/dist/astro/routes/api/content/_collection_/trash.mjs +4 -4
  95. package/dist/astro/routes/api/dashboard.mjs +7 -7
  96. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  97. package/dist/astro/routes/api/import/probe.mjs +4 -4
  98. package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
  99. package/dist/astro/routes/api/import/wordpress/execute.d.mts +2 -2
  100. package/dist/astro/routes/api/import/wordpress/execute.mjs +8 -8
  101. package/dist/astro/routes/api/import/wordpress/media.mjs +4 -4
  102. package/dist/astro/routes/api/import/wordpress/prepare.mjs +6 -6
  103. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +5 -5
  104. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +4 -4
  105. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +6 -6
  106. package/dist/astro/routes/api/manifest.mjs +3 -3
  107. package/dist/astro/routes/api/mcp.mjs +34 -34
  108. package/dist/astro/routes/api/media/_id_/confirm.mjs +4 -4
  109. package/dist/astro/routes/api/media/_id_.mjs +4 -4
  110. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  111. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  112. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  113. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  114. package/dist/astro/routes/api/media/upload-url.mjs +4 -4
  115. package/dist/astro/routes/api/media.mjs +5 -5
  116. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +5 -5
  117. package/dist/astro/routes/api/menus/_name_/items.mjs +5 -5
  118. package/dist/astro/routes/api/menus/_name_/reorder.mjs +5 -5
  119. package/dist/astro/routes/api/menus/_name_/translations.mjs +5 -5
  120. package/dist/astro/routes/api/menus/_name_.mjs +5 -5
  121. package/dist/astro/routes/api/menus/index.mjs +5 -5
  122. package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
  123. package/dist/astro/routes/api/oauth/device/code.mjs +4 -4
  124. package/dist/astro/routes/api/oauth/device/token.mjs +4 -4
  125. package/dist/astro/routes/api/oauth/register.mjs +2 -2
  126. package/dist/astro/routes/api/oauth/token/refresh.mjs +3 -3
  127. package/dist/astro/routes/api/oauth/token/revoke.mjs +3 -3
  128. package/dist/astro/routes/api/oauth/token.mjs +2 -2
  129. package/dist/astro/routes/api/openapi.json.mjs +2 -2
  130. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
  131. package/dist/astro/routes/api/redirects/404s/index.mjs +6 -6
  132. package/dist/astro/routes/api/redirects/404s/summary.mjs +6 -6
  133. package/dist/astro/routes/api/redirects/_id_.mjs +7 -7
  134. package/dist/astro/routes/api/redirects/index.mjs +7 -7
  135. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  136. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  137. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +26 -26
  138. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +26 -26
  139. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +26 -26
  140. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +26 -26
  141. package/dist/astro/routes/api/schema/collections/index.mjs +26 -26
  142. package/dist/astro/routes/api/schema/index.mjs +6 -6
  143. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +26 -26
  144. package/dist/astro/routes/api/schema/orphans/index.mjs +26 -26
  145. package/dist/astro/routes/api/search/enable.mjs +7 -7
  146. package/dist/astro/routes/api/search/index.mjs +6 -6
  147. package/dist/astro/routes/api/search/rebuild.mjs +7 -7
  148. package/dist/astro/routes/api/search/stats.mjs +6 -6
  149. package/dist/astro/routes/api/search/suggest.mjs +6 -6
  150. package/dist/astro/routes/api/sections/_slug_.mjs +6 -6
  151. package/dist/astro/routes/api/sections/index.mjs +6 -6
  152. package/dist/astro/routes/api/settings/email.mjs +4 -4
  153. package/dist/astro/routes/api/settings.mjs +8 -8
  154. package/dist/astro/routes/api/setup/admin-verify.mjs +3 -3
  155. package/dist/astro/routes/api/setup/admin.mjs +3 -3
  156. package/dist/astro/routes/api/setup/dev-bypass.mjs +16 -16
  157. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  158. package/dist/astro/routes/api/setup/index.mjs +17 -17
  159. package/dist/astro/routes/api/setup/status.mjs +3 -3
  160. package/dist/astro/routes/api/snapshot.mjs +3 -3
  161. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +8 -8
  162. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +8 -8
  163. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +8 -8
  164. package/dist/astro/routes/api/taxonomies/index.mjs +8 -8
  165. package/dist/astro/routes/api/themes/preview.mjs +3 -3
  166. package/dist/astro/routes/api/typegen.mjs +5 -5
  167. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +4 -4
  168. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +6 -6
  169. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +6 -6
  170. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  171. package/dist/astro/routes/api/widget-areas/index.mjs +6 -6
  172. package/dist/astro/routes/api/widget-components.mjs +2 -2
  173. package/dist/astro/routes/robots.txt.mjs +4 -4
  174. package/dist/astro/routes/sitemap-_collection_.xml.mjs +4 -4
  175. package/dist/astro/routes/sitemap.xml.mjs +4 -4
  176. package/dist/astro/types.d.mts +2 -2
  177. package/dist/{authorize-Cse0k7B0.mjs → authorize-B__u-iZJ.mjs} +2 -2
  178. package/dist/{authorize-Cse0k7B0.mjs.map → authorize-B__u-iZJ.mjs.map} +1 -1
  179. package/dist/{byline-DVo-liaP.mjs → byline-C-YajfV7.mjs} +5 -5
  180. package/dist/{byline-DVo-liaP.mjs.map → byline-C-YajfV7.mjs.map} +1 -1
  181. package/dist/{byline-fields-RFPkDLRw.d.mts → byline-fields-Ca56gaT6.d.mts} +6 -6
  182. package/dist/{byline-fields-RFPkDLRw.d.mts.map → byline-fields-Ca56gaT6.d.mts.map} +1 -1
  183. package/dist/{bylines-f5ppBpD5.mjs → bylines-BMtJnPnG.mjs} +5 -5
  184. package/dist/{bylines-f5ppBpD5.mjs.map → bylines-BMtJnPnG.mjs.map} +1 -1
  185. package/dist/{bylines-pIKgc_TL.mjs → bylines-D4KADDAi.mjs} +4 -4
  186. package/dist/{bylines-pIKgc_TL.mjs.map → bylines-D4KADDAi.mjs.map} +1 -1
  187. package/dist/{cache-Q6V4U-Xz.mjs → cache-BUJe2w6Z.mjs} +2 -2
  188. package/dist/{cache-Q6V4U-Xz.mjs.map → cache-BUJe2w6Z.mjs.map} +1 -1
  189. package/dist/{chunks-SyjZaYwV.mjs → chunks-BHgcHh2C.mjs} +2 -2
  190. package/dist/{chunks-SyjZaYwV.mjs.map → chunks-BHgcHh2C.mjs.map} +1 -1
  191. package/dist/cli/index.mjs +17 -17
  192. package/dist/cli/index.mjs.map +1 -1
  193. package/dist/{comment-80UyJJUR.mjs → comment-BDx50EX5.mjs} +2 -2
  194. package/dist/{comment-80UyJJUR.mjs.map → comment-BDx50EX5.mjs.map} +1 -1
  195. package/dist/{comment-reaction-Dyb-AutY.mjs → comment-reaction-Gv_pgfI3.mjs} +2 -2
  196. package/dist/{comment-reaction-Dyb-AutY.mjs.map → comment-reaction-Gv_pgfI3.mjs.map} +1 -1
  197. package/dist/{comments-Cczx7AvD.mjs → comments-CXHuQ6jI.mjs} +3 -3
  198. package/dist/{comments-Cczx7AvD.mjs.map → comments-CXHuQ6jI.mjs.map} +1 -1
  199. package/dist/{content-2PTDNnoh.mjs → content-PoVGKjp0.mjs} +3 -3
  200. package/dist/{content-2PTDNnoh.mjs.map → content-PoVGKjp0.mjs.map} +1 -1
  201. package/dist/{context-B1W8soLv.mjs → context-Dvws41_A.mjs} +7 -7
  202. package/dist/{context-B1W8soLv.mjs.map → context-Dvws41_A.mjs.map} +1 -1
  203. package/dist/{dashboard-CYjpQZ6-.mjs → dashboard-DsYU8JKS.mjs} +4 -4
  204. package/dist/{dashboard-CYjpQZ6-.mjs.map → dashboard-DsYU8JKS.mjs.map} +1 -1
  205. package/dist/db/index.mjs +1 -1
  206. package/dist/{error-Bl6kZgtt.mjs → error-lq_fL-RS.mjs} +2 -2
  207. package/dist/{error-Bl6kZgtt.mjs.map → error-lq_fL-RS.mjs.map} +1 -1
  208. package/dist/{field-defs-cache-ztrdzUGb.mjs → field-defs-cache-CYYx8xlG.mjs} +2 -2
  209. package/dist/{field-defs-cache-ztrdzUGb.mjs.map → field-defs-cache-CYYx8xlG.mjs.map} +1 -1
  210. package/dist/{fts-manager-CQuHt-vS.mjs → fts-manager-DW_NR-Fs.mjs} +2 -2
  211. package/dist/{fts-manager-CQuHt-vS.mjs.map → fts-manager-DW_NR-Fs.mjs.map} +1 -1
  212. package/dist/{index-BaIj5hIq.d.mts → index-BroXT-vD.d.mts} +98 -22
  213. package/dist/index-BroXT-vD.d.mts.map +1 -0
  214. package/dist/index.d.mts +2 -2
  215. package/dist/index.mjs +36 -36
  216. package/dist/{load-BNo-ER8C.mjs → load-DMFi4n1p.mjs} +2 -2
  217. package/dist/{load-BNo-ER8C.mjs.map → load-DMFi4n1p.mjs.map} +1 -1
  218. package/dist/{loader-Dn74CG1b.mjs → loader-BR2SgfEc.mjs} +10 -4
  219. package/dist/loader-BR2SgfEc.mjs.map +1 -0
  220. package/dist/media/local-runtime.d.mts +2 -2
  221. package/dist/media/local-runtime.mjs +4 -4
  222. package/dist/{media-Bw0sA6rb.mjs → media-CTjJoJTh.mjs} +2 -2
  223. package/dist/{media-Bw0sA6rb.mjs.map → media-CTjJoJTh.mjs.map} +1 -1
  224. package/dist/{menus-CYs0WZoL.mjs → menus-8O6xNcYW.mjs} +2 -2
  225. package/dist/{menus-CYs0WZoL.mjs.map → menus-8O6xNcYW.mjs.map} +1 -1
  226. package/dist/{menus-DS8VTZgo.mjs → menus-GvRSpgIA.mjs} +5 -5
  227. package/dist/{menus-DS8VTZgo.mjs.map → menus-GvRSpgIA.mjs.map} +1 -1
  228. package/dist/{parse-C9hpjUyY.mjs → parse-DHI873qU.mjs} +2 -2
  229. package/dist/{parse-C9hpjUyY.mjs.map → parse-DHI873qU.mjs.map} +1 -1
  230. package/dist/plugin-utils.d.mts +2 -2
  231. package/dist/plugins/adapt-sandbox-entry.d.mts +2 -2
  232. package/dist/{query-DgUHZLWB.mjs → query-DZdlS6Cu.mjs} +23 -16
  233. package/dist/query-DZdlS6Cu.mjs.map +1 -0
  234. package/dist/{rate-limit-BuQ5d4X_.mjs → rate-limit-D1Y6VFVP.mjs} +2 -2
  235. package/dist/{rate-limit-BuQ5d4X_.mjs.map → rate-limit-D1Y6VFVP.mjs.map} +1 -1
  236. package/dist/{redirect-Bifv_yHv.mjs → redirect-C1KVGYWe.mjs} +2 -2
  237. package/dist/{redirect-Bifv_yHv.mjs.map → redirect-C1KVGYWe.mjs.map} +1 -1
  238. package/dist/{redirects-BZ8Mxn8H.mjs → redirects-DgnypVir.mjs} +3 -3
  239. package/dist/{redirects-BZ8Mxn8H.mjs.map → redirects-DgnypVir.mjs.map} +1 -1
  240. package/dist/{registry-CK1SS6pi.mjs → registry-CJkCubOJ.mjs} +4 -4
  241. package/dist/{registry-CK1SS6pi.mjs.map → registry-CJkCubOJ.mjs.map} +1 -1
  242. package/dist/{runner-DYYnW530.mjs → runner-BsquExlK.mjs} +246 -174
  243. package/dist/runner-BsquExlK.mjs.map +1 -0
  244. package/dist/runner-CEtq-6Sm.d.mts.map +1 -1
  245. package/dist/runtime.d.mts +2 -2
  246. package/dist/runtime.mjs +2 -2
  247. package/dist/{schema-CbFRFoM7.mjs → schema-DMjXuvNU.mjs} +4 -4
  248. package/dist/{schema-CbFRFoM7.mjs.map → schema-DMjXuvNU.mjs.map} +1 -1
  249. package/dist/{search-C-waE_YW.mjs → search-D1wKk3lc.mjs} +4 -4
  250. package/dist/{search-C-waE_YW.mjs.map → search-D1wKk3lc.mjs.map} +1 -1
  251. package/dist/{sections-osX9U-YJ.mjs → sections-CCMic8dB.mjs} +3 -3
  252. package/dist/{sections-osX9U-YJ.mjs.map → sections-CCMic8dB.mjs.map} +1 -1
  253. package/dist/seed/index.mjs +14 -14
  254. package/dist/{seo-4FovMwNS.mjs → seo-3QrZ1EPT.mjs} +2 -2
  255. package/dist/{seo-4FovMwNS.mjs.map → seo-3QrZ1EPT.mjs.map} +1 -1
  256. package/dist/{service-jby1RRmU.mjs → service-CQHK_10l.mjs} +2 -2
  257. package/dist/{service-jby1RRmU.mjs.map → service-CQHK_10l.mjs.map} +1 -1
  258. package/dist/{settings-4bRT2NIf.mjs → settings-C3Yi5Co3.mjs} +3 -3
  259. package/dist/{settings-4bRT2NIf.mjs.map → settings-C3Yi5Co3.mjs.map} +1 -1
  260. package/dist/{settings-t-H2jPuJ.mjs → settings-D9rvaLuY.mjs} +3 -3
  261. package/dist/{settings-t-H2jPuJ.mjs.map → settings-D9rvaLuY.mjs.map} +1 -1
  262. package/dist/{taxonomies-Dxrc_szG.mjs → taxonomies-CurpLtNC.mjs} +29 -21
  263. package/dist/taxonomies-CurpLtNC.mjs.map +1 -0
  264. package/dist/{taxonomies-BBB8O-CU.mjs → taxonomies-DhLaNJji.mjs} +34 -27
  265. package/dist/taxonomies-DhLaNJji.mjs.map +1 -0
  266. package/dist/{taxonomy-B2HQuVf5.mjs → taxonomy-BBUlGug5.mjs} +33 -7
  267. package/dist/taxonomy-BBUlGug5.mjs.map +1 -0
  268. package/dist/{types-DXOIbece.mjs → types-CmI1OyQg.mjs} +2 -2
  269. package/dist/{types-DXOIbece.mjs.map → types-CmI1OyQg.mjs.map} +1 -1
  270. package/dist/{user-Db-Rti_z.mjs → user-JAuc8VUf.mjs} +3 -3
  271. package/dist/{user-Db-Rti_z.mjs.map → user-JAuc8VUf.mjs.map} +1 -1
  272. package/dist/{validate-0Utjspbs.mjs → validate-BzXHWnkV.mjs} +2 -2
  273. package/dist/{validate-0Utjspbs.mjs.map → validate-BzXHWnkV.mjs.map} +1 -1
  274. package/dist/{validation-D9EnzH1T.mjs → validation-CY1gk4Ar.mjs} +5 -5
  275. package/dist/{validation-D9EnzH1T.mjs.map → validation-CY1gk4Ar.mjs.map} +1 -1
  276. package/dist/version-C0JPFUG8.mjs +7 -0
  277. package/dist/{version-BgVM_1MU.mjs.map → version-C0JPFUG8.mjs.map} +1 -1
  278. package/dist/{widgets-BSTzPUYa.mjs → widgets-Dri0spI4.mjs} +2 -2
  279. package/dist/{widgets-BSTzPUYa.mjs.map → widgets-Dri0spI4.mjs.map} +1 -1
  280. package/dist/{zod-generator-ElVQOav7.mjs → zod-generator-xEHODMJf.mjs} +2 -2
  281. package/dist/{zod-generator-ElVQOav7.mjs.map → zod-generator-xEHODMJf.mjs.map} +1 -1
  282. package/package.json +6 -6
  283. package/src/api/handlers/taxonomies.ts +62 -32
  284. package/src/astro/integration/shims/use-sync-external-store-with-selector.js +71 -0
  285. package/src/astro/integration/shims/use-sync-external-store.js +1 -0
  286. package/src/astro/integration/vite-config.ts +47 -8
  287. package/src/astro/middleware.ts +13 -3
  288. package/src/cli/commands/export-seed.ts +5 -4
  289. package/src/database/migrations/045_taxonomy_parent_group.ts +68 -0
  290. package/src/database/migrations/runner.ts +2 -0
  291. package/src/database/repositories/taxonomy.ts +50 -7
  292. package/src/loader.ts +64 -8
  293. package/src/query.ts +93 -18
  294. package/src/taxonomies/index.ts +20 -8
  295. package/dist/index-BaIj5hIq.d.mts.map +0 -1
  296. package/dist/loader-Dn74CG1b.mjs.map +0 -1
  297. package/dist/query-DgUHZLWB.mjs.map +0 -1
  298. package/dist/runner-DYYnW530.mjs.map +0 -1
  299. package/dist/taxonomies-BBB8O-CU.mjs.map +0 -1
  300. package/dist/taxonomies-Dxrc_szG.mjs.map +0 -1
  301. package/dist/taxonomy-B2HQuVf5.mjs.map +0 -1
  302. package/dist/version-BgVM_1MU.mjs +0 -7
@@ -1 +1 @@
1
- {"version":3,"file":"zod-generator-ElVQOav7.mjs","names":[],"sources":["../src/schema/zod-generator.ts"],"sourcesContent":["import { z, type ZodTypeAny } from \"zod\";\n\nimport { hashString } from \"../utils/hash.js\";\nimport type { Field, FieldType, CollectionWithFields } from \"./types.js\";\n\n/** Pattern to split on underscores, hyphens, and spaces for PascalCase conversion */\nconst PASCAL_CASE_SPLIT_PATTERN = /[_\\-\\s]+/;\n\n/**\n * Generate a Zod schema from a collection's field definitions\n *\n * This allows runtime validation of content based on dynamically\n * defined schemas stored in D1.\n */\nexport function generateZodSchema(\n\tcollection: CollectionWithFields,\n): z.ZodObject<Record<string, ZodTypeAny>> {\n\tconst shape: Record<string, ZodTypeAny> = {};\n\n\tfor (const field of collection.fields) {\n\t\tshape[field.slug] = generateFieldSchema(field);\n\t}\n\n\treturn z.object(shape);\n}\n\n/**\n * Generate Zod schema for a single field\n */\nexport function generateFieldSchema(field: Field): ZodTypeAny {\n\tlet schema = getBaseSchema(field.type, field);\n\n\t// Apply validation rules\n\tif (field.validation) {\n\t\tschema = applyValidation(schema, field);\n\t}\n\n\t// Apply required/optional. Non-required fields use `.nullish()` rather\n\t// than `.optional()` because the underlying SQLite columns are nullable\n\t// (see `SchemaRegistry.addFieldColumn` -- non-required fields are added\n\t// without `NOT NULL`). The admin re-sends what it loaded from the\n\t// server on autosave, so any field that's actually `null` in the DB\n\t// must round-trip cleanly through the validator. `.optional()` only\n\t// accepts `undefined`; `.nullish()` accepts both `undefined` and\n\t// `null`. (#867 — autosave failures on seeded entries.)\n\tif (!field.required) {\n\t\tschema = schema.nullish();\n\t}\n\n\t// Apply default value\n\tif (field.defaultValue !== undefined) {\n\t\tschema = schema.default(field.defaultValue);\n\t}\n\n\treturn schema;\n}\n\n/**\n * Get base Zod schema for a field type\n */\nfunction getBaseSchema(type: FieldType, field: Field): ZodTypeAny {\n\tswitch (type) {\n\t\tcase \"url\":\n\t\t\treturn z.string().url();\n\n\t\tcase \"string\":\n\t\tcase \"text\":\n\t\tcase \"slug\":\n\t\t\treturn z.string();\n\n\t\tcase \"number\":\n\t\t\treturn z.number();\n\n\t\tcase \"integer\":\n\t\t\treturn z.number().int();\n\n\t\tcase \"boolean\":\n\t\t\t// Boolean fields map to `INTEGER` columns (`FIELD_TYPE_TO_COLUMN`\n\t\t\t// in `schema/types.ts`) and `serializeValue` in\n\t\t\t// `database/repositories/content.ts` writes booleans as 0/1.\n\t\t\t// `deserializeValue` never converts them back, so reads return\n\t\t\t// numbers. Coerce the stored 0/1 shape here so a GET → POST\n\t\t\t// round-trip on a boolean field passes validation. Other inputs\n\t\t\t// (strings, other numbers) fall through to `z.boolean()` and\n\t\t\t// produce its standard rejection.\n\t\t\treturn z.preprocess((v) => (v === 0 || v === 1 ? Boolean(v) : v), z.boolean());\n\n\t\tcase \"datetime\":\n\t\t\treturn z.string().datetime().or(z.string().date());\n\n\t\tcase \"select\": {\n\t\t\tconst options = field.validation?.options;\n\t\t\tif (options && options.length > 0) {\n\t\t\t\tconst [first, ...rest] = options;\n\t\t\t\treturn z.enum([first, ...rest]);\n\t\t\t}\n\t\t\treturn z.string();\n\t\t}\n\n\t\tcase \"multiSelect\": {\n\t\t\tconst multiOptions = field.validation?.options;\n\t\t\tif (multiOptions && multiOptions.length > 0) {\n\t\t\t\tconst [first, ...rest] = multiOptions;\n\t\t\t\treturn z.array(z.enum([first, ...rest]));\n\t\t\t}\n\t\t\treturn z.array(z.string());\n\t\t}\n\n\t\tcase \"portableText\":\n\t\t\t// Portable Text is an array of blocks. We require `_type` because\n\t\t\t// renderers dispatch on it, but `_key` is intentionally optional:\n\t\t\t// it's a UI-layer concern that the editor regenerates on every\n\t\t\t// change (see `PortableTextEditor`), and the rest of this schema\n\t\t\t// uses `.passthrough()` for everything below the top level. Making\n\t\t\t// `_key` strictly required here was an accidentally tight invariant\n\t\t\t// that rejected any seed/import data not authored against the\n\t\t\t// editor (#867 — autosave failures on seeded template content).\n\t\t\treturn z.array(\n\t\t\t\tz\n\t\t\t\t\t.object({\n\t\t\t\t\t\t_type: z.string(),\n\t\t\t\t\t\t_key: z.string().optional(),\n\t\t\t\t\t})\n\t\t\t\t\t.passthrough(),\n\t\t\t);\n\n\t\tcase \"image\":\n\t\t\treturn z.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tsrc: z.string().optional(),\n\t\t\t\talt: z.string().optional(),\n\t\t\t\twidth: z.number().optional(),\n\t\t\t\theight: z.number().optional(),\n\t\t\t\t/** Provider ID (e.g. \"local\", \"cloudflare-images\") */\n\t\t\t\tprovider: z.string().optional(),\n\t\t\t\t/** Admin-side preview URL for external providers (not persisted by plugins) */\n\t\t\t\tpreviewUrl: z.string().optional(),\n\t\t\t\t/** Provider-specific metadata; for local media this carries storageKey */\n\t\t\t\tmeta: z.record(z.string(), z.unknown()).optional(),\n\t\t\t});\n\n\t\tcase \"file\":\n\t\t\treturn z.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tsrc: z.string().optional(),\n\t\t\t\tfilename: z.string().optional(),\n\t\t\t\tmimeType: z.string().optional(),\n\t\t\t\tsize: z.number().optional(),\n\t\t\t\t/** Provider ID (e.g. \"local\", \"s3\") */\n\t\t\t\tprovider: z.string().optional(),\n\t\t\t\t/** Provider-specific metadata; for local media this carries storageKey */\n\t\t\t\tmeta: z.record(z.string(), z.unknown()).optional(),\n\t\t\t});\n\n\t\tcase \"reference\":\n\t\t\treturn z.string(); // Reference ID\n\n\t\tcase \"json\":\n\t\t\treturn z.unknown();\n\n\t\tdefault:\n\t\t\treturn z.unknown();\n\t}\n}\n\n/**\n * Apply validation rules to a schema\n */\nfunction applyValidation(schema: ZodTypeAny, field: Field): ZodTypeAny {\n\tconst validation = field.validation;\n\tif (!validation) return schema;\n\n\t// String validations\n\tif (schema instanceof z.ZodString) {\n\t\tlet strSchema = schema;\n\t\tif (validation.minLength !== undefined) {\n\t\t\tstrSchema = strSchema.min(validation.minLength);\n\t\t}\n\t\tif (validation.maxLength !== undefined) {\n\t\t\tstrSchema = strSchema.max(validation.maxLength);\n\t\t}\n\t\tif (validation.pattern) {\n\t\t\tstrSchema = strSchema.regex(new RegExp(validation.pattern));\n\t\t}\n\t\treturn strSchema;\n\t}\n\n\t// Number validations\n\tif (schema instanceof z.ZodNumber) {\n\t\tlet numSchema = schema;\n\t\tif (validation.min !== undefined) {\n\t\t\tnumSchema = numSchema.min(validation.min);\n\t\t}\n\t\tif (validation.max !== undefined) {\n\t\t\tnumSchema = numSchema.max(validation.max);\n\t\t}\n\t\treturn numSchema;\n\t}\n\n\treturn schema;\n}\n\n/**\n * Schema cache to avoid regenerating schemas on every request\n */\nconst schemaCache = new Map<string, { schema: z.ZodObject<any>; version: string }>();\n\n/**\n * Get or generate a cached schema for a collection\n */\nexport function getCachedSchema(\n\tcollection: CollectionWithFields,\n\tversion?: string,\n): z.ZodObject<any> {\n\tconst cacheKey = collection.slug;\n\tconst cached = schemaCache.get(cacheKey);\n\n\t// If version matches, return cached schema\n\tif (cached && (!version || cached.version === version)) {\n\t\treturn cached.schema;\n\t}\n\n\t// Generate new schema\n\tconst schema = generateZodSchema(collection);\n\n\t// Cache it\n\tschemaCache.set(cacheKey, {\n\t\tschema,\n\t\tversion: version || collection.updatedAt,\n\t});\n\n\treturn schema;\n}\n\n/**\n * Invalidate cached schema for a collection\n */\nexport function invalidateSchemaCache(slug: string): void {\n\tschemaCache.delete(slug);\n}\n\n/**\n * Clear all cached schemas\n */\nexport function clearSchemaCache(): void {\n\tschemaCache.clear();\n}\n\n/**\n * Validate data against a collection's schema\n */\nexport function validateContent(\n\tcollection: CollectionWithFields,\n\tdata: unknown,\n): { success: true; data: unknown } | { success: false; errors: z.ZodError } {\n\tconst schema = getCachedSchema(collection);\n\n\tconst result = schema.safeParse(data);\n\n\tif (result.success) {\n\t\treturn { success: true, data: result.data };\n\t}\n\n\treturn { success: false, errors: result.error };\n}\n\n/**\n * Generate TypeScript interface from field definitions\n * Used by CLI `emdash types` to generate types\n */\nexport function generateTypeScript(\n\tcollection: CollectionWithFields,\n\tinterfaceName: string = getInterfaceName(collection),\n): string {\n\tconst lines: string[] = [];\n\n\tlines.push(`export interface ${interfaceName} {`);\n\tlines.push(` id: string;`);\n\tlines.push(` slug: string | null;`);\n\tlines.push(` status: string;`);\n\n\tfor (const field of collection.fields) {\n\t\tconst tsType = fieldTypeToTypeScript(field);\n\t\tconst optional = field.required ? \"\" : \"?\";\n\t\tlines.push(` ${field.slug}${optional}: ${tsType};`);\n\t}\n\n\tlines.push(` createdAt: Date;`);\n\tlines.push(` updatedAt: Date;`);\n\tlines.push(` publishedAt: Date | null;`);\n\t// Bylines are eagerly loaded by getEmDashCollection/getEmDashEntry\n\tlines.push(` bylines?: ContentBylineCredit[];`);\n\t// Taxonomy terms are eagerly loaded by getEmDashCollection/getEmDashEntry,\n\t// keyed by taxonomy name (e.g. data.terms?.tag)\n\tlines.push(` terms?: Record<string, TaxonomyTerm[]>;`);\n\tlines.push(`}`);\n\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Generate a complete types file with module augmentation\n * This produces emdash-env.d.ts content that provides typed query functions\n */\nexport function generateTypesFile(collections: CollectionWithFields[]): string {\n\tconst lines: string[] = [];\n\n\t// Header\n\tlines.push(`// Generated by EmDash on dev server start`);\n\tlines.push(`// Do not edit manually`);\n\tlines.push(``);\n\tlines.push(`/// <reference types=\"emdash/locals\" />`);\n\tlines.push(``);\n\n\t// Check if we need PortableTextBlock import\n\tconst needsPortableText = collections.some((c) =>\n\t\tc.fields.some((f) => f.type === \"portableText\"),\n\t);\n\n\t// Build imports - ContentBylineCredit and TaxonomyTerm are always needed\n\t// for the hydrated bylines/terms fields\n\tconst imports = [\"ContentBylineCredit\", \"TaxonomyTerm\"];\n\tif (needsPortableText) {\n\t\timports.push(\"PortableTextBlock\");\n\t}\n\tlines.push(`import type { ${imports.join(\", \")} } from \"emdash\";`);\n\tlines.push(``);\n\n\t// Singularizing the slug can map two distinct slugs to the same name\n\t// (e.g. `book` and `books` both -> `Book`), so resolve collisions up front\n\t// to keep every interface identifier unique within the file.\n\tconst interfaceNames = uniqueInterfaceNames(collections);\n\n\t// Generate individual interfaces\n\tfor (const collection of collections) {\n\t\tlines.push(generateTypeScript(collection, interfaceNames.get(collection.slug)));\n\t\tlines.push(``);\n\t}\n\n\t// Generate the Collections interface for module augmentation\n\tlines.push(`declare module \"emdash\" {`);\n\tlines.push(` interface EmDashCollections {`);\n\tfor (const collection of collections) {\n\t\tlines.push(` ${collection.slug}: ${interfaceNames.get(collection.slug)};`);\n\t}\n\tlines.push(` }`);\n\tlines.push(`}`);\n\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Generate schema hash for cache invalidation\n */\nexport async function generateSchemaHash(collections: CollectionWithFields[]): Promise<string> {\n\tconst str = JSON.stringify(\n\t\tcollections.map((c) => ({\n\t\t\tslug: c.slug,\n\t\t\tfields: c.fields.map((f) => ({\n\t\t\t\tslug: f.slug,\n\t\t\t\ttype: f.type,\n\t\t\t\trequired: f.required,\n\t\t\t\tvalidation: f.validation,\n\t\t\t})),\n\t\t})),\n\t);\n\treturn hashString(str);\n}\n\n/**\n * Map field type to TypeScript type\n */\nfunction fieldTypeToTypeScript(field: Field): string {\n\tswitch (field.type) {\n\t\tcase \"string\":\n\t\tcase \"text\":\n\t\tcase \"slug\":\n\t\tcase \"url\":\n\t\tcase \"datetime\":\n\t\t\treturn \"string\";\n\n\t\tcase \"number\":\n\t\tcase \"integer\":\n\t\t\treturn \"number\";\n\n\t\tcase \"boolean\":\n\t\t\treturn \"boolean\";\n\n\t\tcase \"select\":\n\t\t\tconst options = field.validation?.options;\n\t\t\tif (options && options.length > 0) {\n\t\t\t\treturn options.map((o) => `\"${o}\"`).join(\" | \");\n\t\t\t}\n\t\t\treturn \"string\";\n\n\t\tcase \"multiSelect\":\n\t\t\tconst multiOptions = field.validation?.options;\n\t\t\tif (multiOptions && multiOptions.length > 0) {\n\t\t\t\treturn `(${multiOptions.map((o) => `\"${o}\"`).join(\" | \")})[]`;\n\t\t\t}\n\t\t\treturn \"string[]\";\n\n\t\tcase \"portableText\":\n\t\t\treturn \"PortableTextBlock[]\";\n\n\t\tcase \"image\":\n\t\t\treturn \"{ id: string; src?: string; alt?: string; width?: number; height?: number; provider?: string; previewUrl?: string; meta?: Record<string, unknown> }\";\n\n\t\tcase \"file\":\n\t\t\treturn \"{ id: string; src?: string; filename?: string; mimeType?: string; size?: number; provider?: string; meta?: Record<string, unknown> }\";\n\n\t\tcase \"reference\":\n\t\t\t// Could be enhanced to include the referenced collection type\n\t\t\treturn \"string\";\n\n\t\tcase \"json\":\n\t\t\treturn \"unknown\";\n\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n}\n\n/**\n * Convert string to PascalCase (handles slugs, spaces, etc.)\n */\nfunction pascalCase(str: string): string {\n\treturn str\n\t\t.split(PASCAL_CASE_SPLIT_PATTERN)\n\t\t.filter(Boolean)\n\t\t.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n\t\t.join(\"\");\n}\n\n/**\n * Naive singularization for slug-derived interface names. Handles the common\n * English plural endings; intentionally simple, not a full inflector.\n */\nfunction singularize(str: string): string {\n\tif (str.endsWith(\"ies\")) {\n\t\treturn str.slice(0, -3) + \"y\";\n\t}\n\tif (\n\t\tstr.endsWith(\"es\") &&\n\t\t(str.endsWith(\"sses\") || str.endsWith(\"xes\") || str.endsWith(\"ches\") || str.endsWith(\"shes\"))\n\t) {\n\t\treturn str.slice(0, -2);\n\t}\n\tif (str.endsWith(\"s\") && !str.endsWith(\"ss\")) {\n\t\treturn str.slice(0, -1);\n\t}\n\treturn str;\n}\n\n/**\n * Get the interface name for a collection.\n *\n * Derived from the slug, not the human label. Slugs are constrained to\n * `/^[a-z][a-z0-9_]*$/`, so PascalCasing one always yields a valid TS\n * identifier; labels are arbitrary and user-controlled (punctuation, spaces,\n * duplicates across collections), which produced syntactically invalid or\n * duplicate interface names. The slug is singularized first because the\n * interface describes a single entry, not the collection (`posts` -> `Post`).\n *\n * Singularization can map two distinct slugs onto the same name, so callers\n * generating more than one interface must dedupe -- see `uniqueInterfaceNames`.\n */\nfunction getInterfaceName(collection: CollectionWithFields): string {\n\treturn pascalCase(singularize(collection.slug));\n}\n\n/**\n * Resolve interface names for a set of collections, guaranteeing each is\n * unique within the file. Collisions (from singularization or PascalCasing\n * collapsing distinct slugs) get a numeric suffix in collection order, so the\n * generated `.d.ts` never declares two interfaces with the same identifier.\n *\n * The suffix is chosen against the set of names already emitted, not a\n * per-base counter, so a generated name can't collide with another slug's\n * base name (e.g. slugs `book`, `books`, `book2`: `books` -> `Book2` would\n * clash with `book2`, so it advances to `Book3`).\n */\nexport function uniqueInterfaceNames(collections: CollectionWithFields[]): Map<string, string> {\n\tconst used = new Set<string>();\n\tconst names = new Map<string, string>();\n\tfor (const collection of collections) {\n\t\tconst base = getInterfaceName(collection);\n\t\tlet name = base;\n\t\tlet suffix = 2;\n\t\twhile (used.has(name)) {\n\t\t\tname = `${base}${suffix}`;\n\t\t\tsuffix++;\n\t\t}\n\t\tused.add(name);\n\t\tnames.set(collection.slug, name);\n\t}\n\treturn names;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"zod-generator-xEHODMJf.mjs","names":[],"sources":["../src/schema/zod-generator.ts"],"sourcesContent":["import { z, type ZodTypeAny } from \"zod\";\n\nimport { hashString } from \"../utils/hash.js\";\nimport type { Field, FieldType, CollectionWithFields } from \"./types.js\";\n\n/** Pattern to split on underscores, hyphens, and spaces for PascalCase conversion */\nconst PASCAL_CASE_SPLIT_PATTERN = /[_\\-\\s]+/;\n\n/**\n * Generate a Zod schema from a collection's field definitions\n *\n * This allows runtime validation of content based on dynamically\n * defined schemas stored in D1.\n */\nexport function generateZodSchema(\n\tcollection: CollectionWithFields,\n): z.ZodObject<Record<string, ZodTypeAny>> {\n\tconst shape: Record<string, ZodTypeAny> = {};\n\n\tfor (const field of collection.fields) {\n\t\tshape[field.slug] = generateFieldSchema(field);\n\t}\n\n\treturn z.object(shape);\n}\n\n/**\n * Generate Zod schema for a single field\n */\nexport function generateFieldSchema(field: Field): ZodTypeAny {\n\tlet schema = getBaseSchema(field.type, field);\n\n\t// Apply validation rules\n\tif (field.validation) {\n\t\tschema = applyValidation(schema, field);\n\t}\n\n\t// Apply required/optional. Non-required fields use `.nullish()` rather\n\t// than `.optional()` because the underlying SQLite columns are nullable\n\t// (see `SchemaRegistry.addFieldColumn` -- non-required fields are added\n\t// without `NOT NULL`). The admin re-sends what it loaded from the\n\t// server on autosave, so any field that's actually `null` in the DB\n\t// must round-trip cleanly through the validator. `.optional()` only\n\t// accepts `undefined`; `.nullish()` accepts both `undefined` and\n\t// `null`. (#867 — autosave failures on seeded entries.)\n\tif (!field.required) {\n\t\tschema = schema.nullish();\n\t}\n\n\t// Apply default value\n\tif (field.defaultValue !== undefined) {\n\t\tschema = schema.default(field.defaultValue);\n\t}\n\n\treturn schema;\n}\n\n/**\n * Get base Zod schema for a field type\n */\nfunction getBaseSchema(type: FieldType, field: Field): ZodTypeAny {\n\tswitch (type) {\n\t\tcase \"url\":\n\t\t\treturn z.string().url();\n\n\t\tcase \"string\":\n\t\tcase \"text\":\n\t\tcase \"slug\":\n\t\t\treturn z.string();\n\n\t\tcase \"number\":\n\t\t\treturn z.number();\n\n\t\tcase \"integer\":\n\t\t\treturn z.number().int();\n\n\t\tcase \"boolean\":\n\t\t\t// Boolean fields map to `INTEGER` columns (`FIELD_TYPE_TO_COLUMN`\n\t\t\t// in `schema/types.ts`) and `serializeValue` in\n\t\t\t// `database/repositories/content.ts` writes booleans as 0/1.\n\t\t\t// `deserializeValue` never converts them back, so reads return\n\t\t\t// numbers. Coerce the stored 0/1 shape here so a GET → POST\n\t\t\t// round-trip on a boolean field passes validation. Other inputs\n\t\t\t// (strings, other numbers) fall through to `z.boolean()` and\n\t\t\t// produce its standard rejection.\n\t\t\treturn z.preprocess((v) => (v === 0 || v === 1 ? Boolean(v) : v), z.boolean());\n\n\t\tcase \"datetime\":\n\t\t\treturn z.string().datetime().or(z.string().date());\n\n\t\tcase \"select\": {\n\t\t\tconst options = field.validation?.options;\n\t\t\tif (options && options.length > 0) {\n\t\t\t\tconst [first, ...rest] = options;\n\t\t\t\treturn z.enum([first, ...rest]);\n\t\t\t}\n\t\t\treturn z.string();\n\t\t}\n\n\t\tcase \"multiSelect\": {\n\t\t\tconst multiOptions = field.validation?.options;\n\t\t\tif (multiOptions && multiOptions.length > 0) {\n\t\t\t\tconst [first, ...rest] = multiOptions;\n\t\t\t\treturn z.array(z.enum([first, ...rest]));\n\t\t\t}\n\t\t\treturn z.array(z.string());\n\t\t}\n\n\t\tcase \"portableText\":\n\t\t\t// Portable Text is an array of blocks. We require `_type` because\n\t\t\t// renderers dispatch on it, but `_key` is intentionally optional:\n\t\t\t// it's a UI-layer concern that the editor regenerates on every\n\t\t\t// change (see `PortableTextEditor`), and the rest of this schema\n\t\t\t// uses `.passthrough()` for everything below the top level. Making\n\t\t\t// `_key` strictly required here was an accidentally tight invariant\n\t\t\t// that rejected any seed/import data not authored against the\n\t\t\t// editor (#867 — autosave failures on seeded template content).\n\t\t\treturn z.array(\n\t\t\t\tz\n\t\t\t\t\t.object({\n\t\t\t\t\t\t_type: z.string(),\n\t\t\t\t\t\t_key: z.string().optional(),\n\t\t\t\t\t})\n\t\t\t\t\t.passthrough(),\n\t\t\t);\n\n\t\tcase \"image\":\n\t\t\treturn z.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tsrc: z.string().optional(),\n\t\t\t\talt: z.string().optional(),\n\t\t\t\twidth: z.number().optional(),\n\t\t\t\theight: z.number().optional(),\n\t\t\t\t/** Provider ID (e.g. \"local\", \"cloudflare-images\") */\n\t\t\t\tprovider: z.string().optional(),\n\t\t\t\t/** Admin-side preview URL for external providers (not persisted by plugins) */\n\t\t\t\tpreviewUrl: z.string().optional(),\n\t\t\t\t/** Provider-specific metadata; for local media this carries storageKey */\n\t\t\t\tmeta: z.record(z.string(), z.unknown()).optional(),\n\t\t\t});\n\n\t\tcase \"file\":\n\t\t\treturn z.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tsrc: z.string().optional(),\n\t\t\t\tfilename: z.string().optional(),\n\t\t\t\tmimeType: z.string().optional(),\n\t\t\t\tsize: z.number().optional(),\n\t\t\t\t/** Provider ID (e.g. \"local\", \"s3\") */\n\t\t\t\tprovider: z.string().optional(),\n\t\t\t\t/** Provider-specific metadata; for local media this carries storageKey */\n\t\t\t\tmeta: z.record(z.string(), z.unknown()).optional(),\n\t\t\t});\n\n\t\tcase \"reference\":\n\t\t\treturn z.string(); // Reference ID\n\n\t\tcase \"json\":\n\t\t\treturn z.unknown();\n\n\t\tdefault:\n\t\t\treturn z.unknown();\n\t}\n}\n\n/**\n * Apply validation rules to a schema\n */\nfunction applyValidation(schema: ZodTypeAny, field: Field): ZodTypeAny {\n\tconst validation = field.validation;\n\tif (!validation) return schema;\n\n\t// String validations\n\tif (schema instanceof z.ZodString) {\n\t\tlet strSchema = schema;\n\t\tif (validation.minLength !== undefined) {\n\t\t\tstrSchema = strSchema.min(validation.minLength);\n\t\t}\n\t\tif (validation.maxLength !== undefined) {\n\t\t\tstrSchema = strSchema.max(validation.maxLength);\n\t\t}\n\t\tif (validation.pattern) {\n\t\t\tstrSchema = strSchema.regex(new RegExp(validation.pattern));\n\t\t}\n\t\treturn strSchema;\n\t}\n\n\t// Number validations\n\tif (schema instanceof z.ZodNumber) {\n\t\tlet numSchema = schema;\n\t\tif (validation.min !== undefined) {\n\t\t\tnumSchema = numSchema.min(validation.min);\n\t\t}\n\t\tif (validation.max !== undefined) {\n\t\t\tnumSchema = numSchema.max(validation.max);\n\t\t}\n\t\treturn numSchema;\n\t}\n\n\treturn schema;\n}\n\n/**\n * Schema cache to avoid regenerating schemas on every request\n */\nconst schemaCache = new Map<string, { schema: z.ZodObject<any>; version: string }>();\n\n/**\n * Get or generate a cached schema for a collection\n */\nexport function getCachedSchema(\n\tcollection: CollectionWithFields,\n\tversion?: string,\n): z.ZodObject<any> {\n\tconst cacheKey = collection.slug;\n\tconst cached = schemaCache.get(cacheKey);\n\n\t// If version matches, return cached schema\n\tif (cached && (!version || cached.version === version)) {\n\t\treturn cached.schema;\n\t}\n\n\t// Generate new schema\n\tconst schema = generateZodSchema(collection);\n\n\t// Cache it\n\tschemaCache.set(cacheKey, {\n\t\tschema,\n\t\tversion: version || collection.updatedAt,\n\t});\n\n\treturn schema;\n}\n\n/**\n * Invalidate cached schema for a collection\n */\nexport function invalidateSchemaCache(slug: string): void {\n\tschemaCache.delete(slug);\n}\n\n/**\n * Clear all cached schemas\n */\nexport function clearSchemaCache(): void {\n\tschemaCache.clear();\n}\n\n/**\n * Validate data against a collection's schema\n */\nexport function validateContent(\n\tcollection: CollectionWithFields,\n\tdata: unknown,\n): { success: true; data: unknown } | { success: false; errors: z.ZodError } {\n\tconst schema = getCachedSchema(collection);\n\n\tconst result = schema.safeParse(data);\n\n\tif (result.success) {\n\t\treturn { success: true, data: result.data };\n\t}\n\n\treturn { success: false, errors: result.error };\n}\n\n/**\n * Generate TypeScript interface from field definitions\n * Used by CLI `emdash types` to generate types\n */\nexport function generateTypeScript(\n\tcollection: CollectionWithFields,\n\tinterfaceName: string = getInterfaceName(collection),\n): string {\n\tconst lines: string[] = [];\n\n\tlines.push(`export interface ${interfaceName} {`);\n\tlines.push(` id: string;`);\n\tlines.push(` slug: string | null;`);\n\tlines.push(` status: string;`);\n\n\tfor (const field of collection.fields) {\n\t\tconst tsType = fieldTypeToTypeScript(field);\n\t\tconst optional = field.required ? \"\" : \"?\";\n\t\tlines.push(` ${field.slug}${optional}: ${tsType};`);\n\t}\n\n\tlines.push(` createdAt: Date;`);\n\tlines.push(` updatedAt: Date;`);\n\tlines.push(` publishedAt: Date | null;`);\n\t// Bylines are eagerly loaded by getEmDashCollection/getEmDashEntry\n\tlines.push(` bylines?: ContentBylineCredit[];`);\n\t// Taxonomy terms are eagerly loaded by getEmDashCollection/getEmDashEntry,\n\t// keyed by taxonomy name (e.g. data.terms?.tag)\n\tlines.push(` terms?: Record<string, TaxonomyTerm[]>;`);\n\tlines.push(`}`);\n\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Generate a complete types file with module augmentation\n * This produces emdash-env.d.ts content that provides typed query functions\n */\nexport function generateTypesFile(collections: CollectionWithFields[]): string {\n\tconst lines: string[] = [];\n\n\t// Header\n\tlines.push(`// Generated by EmDash on dev server start`);\n\tlines.push(`// Do not edit manually`);\n\tlines.push(``);\n\tlines.push(`/// <reference types=\"emdash/locals\" />`);\n\tlines.push(``);\n\n\t// Check if we need PortableTextBlock import\n\tconst needsPortableText = collections.some((c) =>\n\t\tc.fields.some((f) => f.type === \"portableText\"),\n\t);\n\n\t// Build imports - ContentBylineCredit and TaxonomyTerm are always needed\n\t// for the hydrated bylines/terms fields\n\tconst imports = [\"ContentBylineCredit\", \"TaxonomyTerm\"];\n\tif (needsPortableText) {\n\t\timports.push(\"PortableTextBlock\");\n\t}\n\tlines.push(`import type { ${imports.join(\", \")} } from \"emdash\";`);\n\tlines.push(``);\n\n\t// Singularizing the slug can map two distinct slugs to the same name\n\t// (e.g. `book` and `books` both -> `Book`), so resolve collisions up front\n\t// to keep every interface identifier unique within the file.\n\tconst interfaceNames = uniqueInterfaceNames(collections);\n\n\t// Generate individual interfaces\n\tfor (const collection of collections) {\n\t\tlines.push(generateTypeScript(collection, interfaceNames.get(collection.slug)));\n\t\tlines.push(``);\n\t}\n\n\t// Generate the Collections interface for module augmentation\n\tlines.push(`declare module \"emdash\" {`);\n\tlines.push(` interface EmDashCollections {`);\n\tfor (const collection of collections) {\n\t\tlines.push(` ${collection.slug}: ${interfaceNames.get(collection.slug)};`);\n\t}\n\tlines.push(` }`);\n\tlines.push(`}`);\n\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Generate schema hash for cache invalidation\n */\nexport async function generateSchemaHash(collections: CollectionWithFields[]): Promise<string> {\n\tconst str = JSON.stringify(\n\t\tcollections.map((c) => ({\n\t\t\tslug: c.slug,\n\t\t\tfields: c.fields.map((f) => ({\n\t\t\t\tslug: f.slug,\n\t\t\t\ttype: f.type,\n\t\t\t\trequired: f.required,\n\t\t\t\tvalidation: f.validation,\n\t\t\t})),\n\t\t})),\n\t);\n\treturn hashString(str);\n}\n\n/**\n * Map field type to TypeScript type\n */\nfunction fieldTypeToTypeScript(field: Field): string {\n\tswitch (field.type) {\n\t\tcase \"string\":\n\t\tcase \"text\":\n\t\tcase \"slug\":\n\t\tcase \"url\":\n\t\tcase \"datetime\":\n\t\t\treturn \"string\";\n\n\t\tcase \"number\":\n\t\tcase \"integer\":\n\t\t\treturn \"number\";\n\n\t\tcase \"boolean\":\n\t\t\treturn \"boolean\";\n\n\t\tcase \"select\":\n\t\t\tconst options = field.validation?.options;\n\t\t\tif (options && options.length > 0) {\n\t\t\t\treturn options.map((o) => `\"${o}\"`).join(\" | \");\n\t\t\t}\n\t\t\treturn \"string\";\n\n\t\tcase \"multiSelect\":\n\t\t\tconst multiOptions = field.validation?.options;\n\t\t\tif (multiOptions && multiOptions.length > 0) {\n\t\t\t\treturn `(${multiOptions.map((o) => `\"${o}\"`).join(\" | \")})[]`;\n\t\t\t}\n\t\t\treturn \"string[]\";\n\n\t\tcase \"portableText\":\n\t\t\treturn \"PortableTextBlock[]\";\n\n\t\tcase \"image\":\n\t\t\treturn \"{ id: string; src?: string; alt?: string; width?: number; height?: number; provider?: string; previewUrl?: string; meta?: Record<string, unknown> }\";\n\n\t\tcase \"file\":\n\t\t\treturn \"{ id: string; src?: string; filename?: string; mimeType?: string; size?: number; provider?: string; meta?: Record<string, unknown> }\";\n\n\t\tcase \"reference\":\n\t\t\t// Could be enhanced to include the referenced collection type\n\t\t\treturn \"string\";\n\n\t\tcase \"json\":\n\t\t\treturn \"unknown\";\n\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n}\n\n/**\n * Convert string to PascalCase (handles slugs, spaces, etc.)\n */\nfunction pascalCase(str: string): string {\n\treturn str\n\t\t.split(PASCAL_CASE_SPLIT_PATTERN)\n\t\t.filter(Boolean)\n\t\t.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n\t\t.join(\"\");\n}\n\n/**\n * Naive singularization for slug-derived interface names. Handles the common\n * English plural endings; intentionally simple, not a full inflector.\n */\nfunction singularize(str: string): string {\n\tif (str.endsWith(\"ies\")) {\n\t\treturn str.slice(0, -3) + \"y\";\n\t}\n\tif (\n\t\tstr.endsWith(\"es\") &&\n\t\t(str.endsWith(\"sses\") || str.endsWith(\"xes\") || str.endsWith(\"ches\") || str.endsWith(\"shes\"))\n\t) {\n\t\treturn str.slice(0, -2);\n\t}\n\tif (str.endsWith(\"s\") && !str.endsWith(\"ss\")) {\n\t\treturn str.slice(0, -1);\n\t}\n\treturn str;\n}\n\n/**\n * Get the interface name for a collection.\n *\n * Derived from the slug, not the human label. Slugs are constrained to\n * `/^[a-z][a-z0-9_]*$/`, so PascalCasing one always yields a valid TS\n * identifier; labels are arbitrary and user-controlled (punctuation, spaces,\n * duplicates across collections), which produced syntactically invalid or\n * duplicate interface names. The slug is singularized first because the\n * interface describes a single entry, not the collection (`posts` -> `Post`).\n *\n * Singularization can map two distinct slugs onto the same name, so callers\n * generating more than one interface must dedupe -- see `uniqueInterfaceNames`.\n */\nfunction getInterfaceName(collection: CollectionWithFields): string {\n\treturn pascalCase(singularize(collection.slug));\n}\n\n/**\n * Resolve interface names for a set of collections, guaranteeing each is\n * unique within the file. Collisions (from singularization or PascalCasing\n * collapsing distinct slugs) get a numeric suffix in collection order, so the\n * generated `.d.ts` never declares two interfaces with the same identifier.\n *\n * The suffix is chosen against the set of names already emitted, not a\n * per-base counter, so a generated name can't collide with another slug's\n * base name (e.g. slugs `book`, `books`, `book2`: `books` -> `Book2` would\n * clash with `book2`, so it advances to `Book3`).\n */\nexport function uniqueInterfaceNames(collections: CollectionWithFields[]): Map<string, string> {\n\tconst used = new Set<string>();\n\tconst names = new Map<string, string>();\n\tfor (const collection of collections) {\n\t\tconst base = getInterfaceName(collection);\n\t\tlet name = base;\n\t\tlet suffix = 2;\n\t\twhile (used.has(name)) {\n\t\t\tname = `${base}${suffix}`;\n\t\t\tsuffix++;\n\t\t}\n\t\tused.add(name);\n\t\tnames.set(collection.slug, name);\n\t}\n\treturn names;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emdash",
3
- "version": "0.23.0",
3
+ "version": "0.24.1",
4
4
  "description": "Astro-native CMS with WordPress migration support",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -230,11 +230,11 @@
230
230
  "upng-js": "^2.1.0",
231
231
  "zod": "^4.4.1",
232
232
  "@atcute/client": "^5.0.0",
233
- "@emdash-cms/admin": "0.23.0",
234
- "@emdash-cms/auth": "0.23.0",
235
- "@emdash-cms/gutenberg-to-portable-text": "0.23.0",
233
+ "@emdash-cms/gutenberg-to-portable-text": "0.24.1",
234
+ "@emdash-cms/auth": "0.24.1",
236
235
  "@emdash-cms/plugin-types": "0.1.0",
237
- "@emdash-cms/registry-client": "0.3.2"
236
+ "@emdash-cms/registry-client": "0.3.2",
237
+ "@emdash-cms/admin": "0.24.1"
238
238
  },
239
239
  "optionalDependencies": {
240
240
  "@libsql/kysely-libsql": "^0.4.0",
@@ -267,7 +267,7 @@
267
267
  "vite": "^6.0.0",
268
268
  "vitest": "^4.1.5",
269
269
  "zod-openapi": "^5.4.6",
270
- "@emdash-cms/blocks": "0.23.0"
270
+ "@emdash-cms/blocks": "0.24.1"
271
271
  },
272
272
  "repository": {
273
273
  "type": "git",
@@ -95,12 +95,21 @@ export interface TaxonomyDefTranslationsResponse {
95
95
  * Build tree structure from flat terms
96
96
  */
97
97
  function buildTree(flatTerms: TermWithCount[]): TermWithCount[] {
98
- const map = new Map<string, TermWithCount>();
98
+ // `parentId` holds the parent's translation_group, so resolve links by group.
99
+ // Key by (locale, group): a child's parent lives in the same locale, and an
100
+ // unfiltered list mixes locales whose translated siblings share a group —
101
+ // keying by group alone would collide and misattach children across locales.
102
+ const byLocaleGroup = new Map<string, TermWithCount>();
99
103
  const roots: TermWithCount[] = [];
100
- for (const term of flatTerms) map.set(term.id, term);
101
104
  for (const term of flatTerms) {
102
- if (term.parentId && map.has(term.parentId)) {
103
- map.get(term.parentId)!.children.push(term);
105
+ byLocaleGroup.set(`${term.locale}::${term.translationGroup ?? term.id}`, term);
106
+ }
107
+ for (const term of flatTerms) {
108
+ const parent = term.parentId
109
+ ? byLocaleGroup.get(`${term.locale}::${term.parentId}`)
110
+ : undefined;
111
+ if (parent) {
112
+ parent.children.push(term);
104
113
  } else {
105
114
  roots.push(term);
106
115
  }
@@ -411,24 +420,20 @@ export async function handleTermList(
411
420
  * - `parentId === null` -> caller intends to detach; no-op here.
412
421
  * - parent must exist (FK exists -> term row not soft-deleted).
413
422
  * - parent must live in the same taxonomy.
414
- * - if `termId` is provided (update path), reject `parentId === termId`
415
- * (self-parent) and walk up the parent chain to detect cycles.
423
+ * - reject a parent in the term's own translation_group (self-parent),
424
+ * including the create path: a translation inherits its source's group, so
425
+ * `selfGroup` carries that prospective group when `termId` is absent.
426
+ * - on update, walk up the parent chain to detect cycles.
416
427
  */
417
428
  async function validateParentTerm(
418
429
  repo: TaxonomyRepository,
419
430
  taxonomyName: string,
420
431
  termId: string | undefined,
421
432
  parentId: string | null | undefined,
433
+ selfGroup?: string | null,
422
434
  ): Promise<{ code: "VALIDATION_ERROR"; message: string } | null> {
423
435
  if (parentId === undefined || parentId === null) return null;
424
436
 
425
- if (termId !== undefined && parentId === termId) {
426
- return {
427
- code: "VALIDATION_ERROR",
428
- message: "A term cannot be its own parent",
429
- };
430
- }
431
-
432
437
  const parent = await repo.findById(parentId);
433
438
  if (!parent) {
434
439
  return {
@@ -443,6 +448,22 @@ async function validateParentTerm(
443
448
  };
444
449
  }
445
450
 
451
+ // Parentage keys off translation_group (what `parent_id` persists), so the
452
+ // self-parent and cycle checks compare groups, not row ids — picking a
453
+ // sibling translation of the term as its parent is still self-parenting.
454
+ const parentGroup = parent.translationGroup ?? parent.id;
455
+ let termGroup: string | null = selfGroup ?? null;
456
+ if (termId !== undefined) {
457
+ const term = await repo.findById(termId);
458
+ termGroup = term ? (term.translationGroup ?? term.id) : termId;
459
+ }
460
+ if (termGroup !== null && parentGroup === termGroup) {
461
+ return {
462
+ code: "VALIDATION_ERROR",
463
+ message: "A term cannot be its own parent",
464
+ };
465
+ }
466
+
446
467
  // Walk up the parent chain. Two checks fold into one walk:
447
468
  // - Cycle detection (only on update — a non-existent term-being-
448
469
  // created can't be its own ancestor): if the walk revisits termId
@@ -454,11 +475,13 @@ async function validateParentTerm(
454
475
  // The depth-exceeded error fires only when we hit the limit AND there
455
476
  // was still chain to walk — a legitimate chain of exactly MAX_DEPTH
456
477
  // ancestors exits with `cursor === null` and is accepted.
478
+ // `parent_id` stores a translation_group, which always equals the anchor
479
+ // row's id, so findById(cursor) still resolves each ancestor.
457
480
  const MAX_DEPTH = 100;
458
481
  let cursor: string | null = parent.parentId;
459
482
  let steps = 0;
460
483
  while (cursor !== null && steps < MAX_DEPTH) {
461
- if (termId !== undefined && cursor === termId) {
484
+ if (termGroup !== null && cursor === termGroup) {
462
485
  return {
463
486
  code: "VALIDATION_ERROR",
464
487
  message: "Cycle detected: cannot make a descendant the parent",
@@ -504,7 +527,7 @@ export async function handleTermCreate(
504
527
  const repo = new TaxonomyRepository(db);
505
528
 
506
529
  // Coerce empty-string parentId to undefined (treat as "no parent").
507
- let parentId =
530
+ const parentId =
508
531
  input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
509
532
 
510
533
  // Conflict check is scoped to locale (per-locale slugs are unique).
@@ -521,27 +544,29 @@ export async function handleTermCreate(
521
544
  };
522
545
  }
523
546
 
524
- // If creating a translation whose parent is the translated sibling of
525
- // the source's parent, try to resolve the parent in the same locale.
526
- if (input.translationOf && parentId) {
547
+ // No locale re-pointing needed: `repo.create` persists the parent's
548
+ // translation_group in `parent_id`, so a child stays nested under the
549
+ // parent in every locale automatically — including parents translated
550
+ // after the child was created.
551
+
552
+ // A translation inherits its source's translation_group; pass that
553
+ // prospective group so validateParentTerm can reject a parent in the same
554
+ // group (cross-locale self-parent) even though the term doesn't exist yet.
555
+ let selfGroup: string | null = null;
556
+ if (input.translationOf) {
527
557
  const source = await repo.findById(input.translationOf);
528
- if (source?.parentId === parentId && input.locale) {
529
- const sourceParent = await repo.findById(parentId);
530
- if (sourceParent?.translationGroup) {
531
- const translatedParent = await db
532
- .selectFrom("taxonomies")
533
- .select("id")
534
- .where("translation_group", "=", sourceParent.translationGroup)
535
- .where("locale", "=", input.locale)
536
- .executeTakeFirst();
537
- if (translatedParent) parentId = translatedParent.id;
538
- }
539
- }
558
+ selfGroup = source ? (source.translationGroup ?? source.id) : null;
540
559
  }
541
560
 
542
561
  // Validate parentId: must exist AND belong to the same taxonomy.
543
562
  // (Cycle check is N/A on create — the term doesn't exist yet.)
544
- const parentError = await validateParentTerm(repo, taxonomyName, undefined, parentId);
563
+ const parentError = await validateParentTerm(
564
+ repo,
565
+ taxonomyName,
566
+ undefined,
567
+ parentId,
568
+ selfGroup,
569
+ );
545
570
  if (parentError) {
546
571
  return { success: false, error: parentError };
547
572
  }
@@ -606,7 +631,9 @@ export async function handleTermGet(
606
631
  }
607
632
 
608
633
  const count = await repo.countEntriesWithTerm(term.id);
609
- const children = await repo.findChildren(term.id);
634
+ // Children share this term's translation_group as their parent_id; scope
635
+ // to the term's own locale so the response stays within one locale's tree.
636
+ const children = await repo.findChildren(term.id, term.locale);
610
637
 
611
638
  return {
612
639
  success: true,
@@ -791,6 +818,9 @@ export async function handleTermDelete(
791
818
  };
792
819
  }
793
820
 
821
+ // Block deletion if the term's group still parents any child in any
822
+ // locale — children store this group in parent_id, so removing the term
823
+ // would orphan them (or null their parent_id via the self-FK).
794
824
  const children = await repo.findChildren(term.id);
795
825
  if (children.length > 0) {
796
826
  return {
@@ -0,0 +1,71 @@
1
+ import * as React from "react";
2
+
3
+ const objectIs =
4
+ typeof Object.is === "function"
5
+ ? Object.is
6
+ : (x, y) => (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
7
+
8
+ export function useSyncExternalStoreWithSelector(
9
+ subscribe,
10
+ getSnapshot,
11
+ getServerSnapshot,
12
+ selector,
13
+ isEqual,
14
+ ) {
15
+ const instRef = React.useRef(null);
16
+ if (instRef.current === null) {
17
+ instRef.current = { hasValue: false, value: null };
18
+ }
19
+ const inst = instRef.current;
20
+
21
+ const [getSelection, getServerSelection] = React.useMemo(() => {
22
+ let hasMemo = false;
23
+ let memoizedSnapshot;
24
+ let memoizedSelection;
25
+
26
+ const memoizedSelector = (nextSnapshot) => {
27
+ if (!hasMemo) {
28
+ hasMemo = true;
29
+ memoizedSnapshot = nextSnapshot;
30
+ const nextSelection = selector(nextSnapshot);
31
+ if (isEqual !== undefined && inst.hasValue) {
32
+ const currentSelection = inst.value;
33
+ if (isEqual(currentSelection, nextSelection)) {
34
+ memoizedSelection = currentSelection;
35
+ return currentSelection;
36
+ }
37
+ }
38
+ memoizedSelection = nextSelection;
39
+ return nextSelection;
40
+ }
41
+
42
+ const previousSelection = memoizedSelection;
43
+ if (objectIs(memoizedSnapshot, nextSnapshot)) {
44
+ return previousSelection;
45
+ }
46
+
47
+ const nextSelection = selector(nextSnapshot);
48
+ if (isEqual !== undefined && isEqual(previousSelection, nextSelection)) {
49
+ memoizedSnapshot = nextSnapshot;
50
+ return previousSelection;
51
+ }
52
+
53
+ memoizedSnapshot = nextSnapshot;
54
+ memoizedSelection = nextSelection;
55
+ return nextSelection;
56
+ };
57
+
58
+ return [
59
+ () => memoizedSelector(getSnapshot()),
60
+ getServerSnapshot === undefined ? undefined : () => memoizedSelector(getServerSnapshot()),
61
+ ];
62
+ }, [getSnapshot, getServerSnapshot, selector, isEqual]);
63
+
64
+ const value = React.useSyncExternalStore(subscribe, getSelection, getServerSelection);
65
+ React.useEffect(() => {
66
+ inst.hasValue = true;
67
+ inst.value = value;
68
+ }, [inst, value]);
69
+ React.useDebugValue(value);
70
+ return value;
71
+ }
@@ -0,0 +1 @@
1
+ export { useSyncExternalStore } from "react";
@@ -145,6 +145,15 @@ function resolveAdminSource(projectRoot: string): string | undefined {
145
145
  return undefined;
146
146
  }
147
147
 
148
+ function resolveIntegrationShim(fileName: string): string {
149
+ const currentDir = dirname(fileURLToPath(import.meta.url));
150
+ const sourceShimPath = resolve(currentDir, "shims", fileName);
151
+ if (existsSync(sourceShimPath)) {
152
+ return sourceShimPath;
153
+ }
154
+ return resolve(currentDir, "..", "..", "src", "astro", "integration", "shims", fileName);
155
+ }
156
+
148
157
  export interface VitePluginOptions {
149
158
  /** Serializable config (database, storage, auth descriptors) */
150
159
  serializableConfig: Record<string, unknown>;
@@ -159,7 +168,10 @@ export interface VitePluginOptions {
159
168
  /**
160
169
  * Creates the EmDash virtual modules Vite plugin.
161
170
  */
162
- export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
171
+ export function createVirtualModulesPlugin(
172
+ options: VitePluginOptions,
173
+ astroCommand: "dev" | "build" | "preview" | "sync",
174
+ ): Plugin {
163
175
  const { serializableConfig, resolvedConfig, pluginDescriptors, astroConfig } = options;
164
176
 
165
177
  let viteCommand: "build" | "serve" | undefined;
@@ -294,8 +306,16 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
294
306
  // Generate scheduler module — a NodeCronScheduler factory on
295
307
  // long-lived runtimes, or null under the Cloudflare adapter where
296
308
  // a Cron Trigger drives scheduled work instead.
309
+ //
310
+ // Decide from Astro's command, not Vite's config.command: the
311
+ // Cloudflare adapter builds the worker bundle via a nested Vite
312
+ // *build* pass even during `astro dev`, so viteCommand reports
313
+ // "build" and would wrongly suppress the in-process timer (#1635).
314
+ // Astro's command stays "dev", which is the only case that should
315
+ // fall through to a NodeCronScheduler.
297
316
  if (id === RESOLVED_VIRTUAL_SCHEDULER_ID) {
298
- return generateSchedulerModule(astroConfig.adapter?.name, viteCommand);
317
+ const schedulerCommand = astroCommand === "dev" ? "serve" : "build";
318
+ return generateSchedulerModule(astroConfig.adapter?.name, schedulerCommand);
299
319
  }
300
320
  },
301
321
  };
@@ -340,6 +360,10 @@ export function createViteConfig(
340
360
 
341
361
  const adminSourcePath = isDev ? resolveAdminSource(projectRoot) : undefined;
342
362
  const useSource = adminSourcePath !== undefined;
363
+ const useSyncExternalStoreShimPath = resolveIntegrationShim("use-sync-external-store.js");
364
+ const useSyncExternalStoreWithSelectorShimPath = resolveIntegrationShim(
365
+ "use-sync-external-store-with-selector.js",
366
+ );
343
367
 
344
368
  return {
345
369
  // Astro SSR routes resolve version.ts from source (not tsdown dist),
@@ -370,19 +394,34 @@ export function createViteConfig(
370
394
  // dep scanner can't reach it for pre-bundling — so the browser is
371
395
  // served raw `module.exports` and hydration fails with
372
396
  // `SyntaxError: ... does not provide an export named
373
- // 'useSyncExternalStore'`. Redirect both shim entry points to the
374
- // main `use-sync-external-store` package, which on React >=18
375
- // (our peer-dep floor) delegates to React's built-in hook.
397
+ // 'useSyncExternalStore'`. Redirect both shim entry points to a
398
+ // tiny ESM shim file that re-exports React's built-in hook, and
399
+ // redirect the selector entry points to an ESM wrapper around that
400
+ // hook. The absolute file paths are required because Vite/Rolldown
401
+ // dependency optimization applies aliases without EmDash's virtual
402
+ // module plugin. This also avoids the package main entry's React
403
+ // 18+ dev warning.
404
+ {
405
+ find: "use-sync-external-store/shim/with-selector.js",
406
+ replacement: useSyncExternalStoreWithSelectorShimPath,
407
+ },
408
+ {
409
+ find: "use-sync-external-store/shim/with-selector",
410
+ replacement: useSyncExternalStoreWithSelectorShimPath,
411
+ },
376
412
  {
377
413
  find: "use-sync-external-store/shim/index.js",
378
- replacement: "use-sync-external-store",
414
+ replacement: useSyncExternalStoreShimPath,
415
+ },
416
+ {
417
+ find: "use-sync-external-store/shim",
418
+ replacement: useSyncExternalStoreShimPath,
379
419
  },
380
- { find: "use-sync-external-store/shim", replacement: "use-sync-external-store" },
381
420
  ],
382
421
  },
383
422
  // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Monorepo has both vite 6 (docs) and vite 7 (core). tsgo resolves correctly.
384
423
  plugins: [
385
- createVirtualModulesPlugin(options),
424
+ createVirtualModulesPlugin(options, command),
386
425
  // In dev mode with source alias, compile Lingui macros on the fly
387
426
  // and redirect locale .mjs imports to dist/.
388
427
  // In production, macros are pre-compiled by tsdown in the admin package.
@@ -355,9 +355,19 @@ function finalizeResponse(
355
355
  if (astroCookies !== undefined) {
356
356
  Reflect.set(res, ASTRO_COOKIES_SYMBOL, astroCookies);
357
357
  }
358
- res.headers.set("X-Content-Type-Options", "nosniff");
359
- res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
360
- res.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()");
358
+ // Set-if-absent so a host app that sets stricter values on its own routes
359
+ // wins. The middleware registers `order: 'pre'` (#1282), so on the response
360
+ // path it runs *after* host middleware; unconditional `set()` would clobber
361
+ // the host's headers on every public route (#1393). Mirrors the CSP guard.
362
+ if (!res.headers.has("X-Content-Type-Options")) {
363
+ res.headers.set("X-Content-Type-Options", "nosniff");
364
+ }
365
+ if (!res.headers.has("Referrer-Policy")) {
366
+ res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
367
+ }
368
+ if (!res.headers.has("Permissions-Policy")) {
369
+ res.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()");
370
+ }
361
371
  if (!res.headers.has("Content-Security-Policy")) {
362
372
  res.headers.set("X-Frame-Options", "SAMEORIGIN");
363
373
  }
@@ -375,9 +375,10 @@ async function exportTaxonomies(
375
375
  // Terms in this def's locale.
376
376
  const terms = await termRepo.findByName(def.name, { locale: def.locale });
377
377
 
378
- // id -> slug for parent resolution within this locale.
379
- const idToSlug = new Map<string, string>();
380
- for (const term of terms) idToSlug.set(term.id, term.slug);
378
+ // translation_group -> slug for parent resolution within this locale.
379
+ // `parentId` stores the parent's translation_group, not a row id.
380
+ const groupToSlug = new Map<string, string>();
381
+ for (const term of terms) groupToSlug.set(term.translationGroup ?? term.id, term.slug);
381
382
 
382
383
  // translation_group -> seed id of the anchor term.
383
384
  const termGroupToSeedId = new Map<string, string>();
@@ -396,7 +397,7 @@ async function exportTaxonomies(
396
397
  description: typeof term.data?.description === "string" ? term.data.description : undefined,
397
398
  };
398
399
 
399
- if (term.parentId) seedTerm.parent = idToSlug.get(term.parentId);
400
+ if (term.parentId) seedTerm.parent = groupToSlug.get(term.parentId);
400
401
 
401
402
  if (i18nEnabled && term.locale) {
402
403
  seedTerm.locale = term.locale;
@@ -0,0 +1,68 @@
1
+ import type { Kysely } from "kysely";
2
+ import { sql } from "kysely";
3
+
4
+ /**
5
+ * Store the parent's `translation_group` in `taxonomies.parent_id` instead of a
6
+ * locale-bound row id.
7
+ *
8
+ * Before this, a term's `parent_id` pointed at one specific parent *row* (one
9
+ * locale). A child translated in another locale could end up parented to a row
10
+ * in a different locale, silently flattening that locale's tree (#1347). Every
11
+ * other taxonomy i18n relationship already keys off `translation_group`
12
+ * (`content_taxonomies.taxonomy_id`, `_emdash_menu_items.reference_id`), so this
13
+ * aligns parentage with that model: a child links to the parent *concept* and
14
+ * stays nested in whichever locale that parent has been translated into.
15
+ *
16
+ * Backfill rewrites every existing `parent_id` from the referenced parent row's
17
+ * id to that parent's `translation_group`. A `translation_group` normally equals
18
+ * its anchor row's id, so the self-FK on `parent_id` stays valid. We only
19
+ * rewrite when that anchor row still exists; if a parent's anchor was deleted
20
+ * but a sibling translation survives, the existing locale-bound id is left as-is
21
+ * rather than rewritten to a dangling FK value. Such a child then renders as a
22
+ * root rather than nested — an accepted degradation for an already-inconsistent
23
+ * dataset (its `translation_group` already points at a deleted row, which breaks
24
+ * `content_taxonomies` joins too). New deletes can't recreate this state: a
25
+ * parent with children in any locale is undeletable. The correlated subquery is
26
+ * a no-op for rows that already hold a group (an anchor row's id resolves to
27
+ * itself), so the migration is safe to re-run.
28
+ *
29
+ * Dialect-independent: the correlated-subquery `UPDATE` runs on SQLite (incl.
30
+ * D1) and Postgres alike.
31
+ */
32
+ export async function up(db: Kysely<unknown>): Promise<void> {
33
+ await sql`
34
+ UPDATE taxonomies
35
+ SET parent_id = (
36
+ SELECT p.translation_group FROM taxonomies p WHERE p.id = taxonomies.parent_id
37
+ )
38
+ WHERE parent_id IS NOT NULL
39
+ AND EXISTS (
40
+ SELECT 1 FROM taxonomies p
41
+ WHERE p.id = taxonomies.parent_id
42
+ AND p.translation_group IS NOT NULL
43
+ -- Only rewrite to a translation_group that is itself a live row
44
+ -- id, so the self-FK on parent_id can never dangle.
45
+ AND EXISTS (SELECT 1 FROM taxonomies a WHERE a.id = p.translation_group)
46
+ )
47
+ `.execute(db);
48
+ }
49
+
50
+ /**
51
+ * Map each `parent_id` group back to the row id of the parent in the same
52
+ * locale (the pre-migration shape). Rows with no same-locale parent are left as
53
+ * they are rather than nulled, so a rollback never silently detaches a subtree.
54
+ */
55
+ export async function down(db: Kysely<unknown>): Promise<void> {
56
+ await sql`
57
+ UPDATE taxonomies
58
+ SET parent_id = COALESCE(
59
+ (
60
+ SELECT c.id FROM taxonomies c
61
+ WHERE c.translation_group = taxonomies.parent_id
62
+ AND c.locale = taxonomies.locale
63
+ ),
64
+ parent_id
65
+ )
66
+ WHERE parent_id IS NOT NULL
67
+ `.execute(db);
68
+ }
@@ -46,6 +46,7 @@ import * as m041 from "./041_content_locale_list_index.js";
46
46
  import * as m042 from "./042_byline_fields.js";
47
47
  import * as m043 from "./043_content_references.js";
48
48
  import * as m044 from "./044_comment_reactions.js";
49
+ import * as m045 from "./045_taxonomy_parent_group.js";
49
50
 
50
51
  const MIGRATIONS: Readonly<Record<string, Migration>> = Object.freeze({
51
52
  "001_initial": m001,
@@ -91,6 +92,7 @@ const MIGRATIONS: Readonly<Record<string, Migration>> = Object.freeze({
91
92
  "042_byline_fields": m042,
92
93
  "043_content_references": m043,
93
94
  "044_comment_reactions": m044,
95
+ "045_taxonomy_parent_group": m045,
94
96
  });
95
97
 
96
98
  /** Total number of registered migrations. Exported for use in tests. */