emdash 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/route-utils.mjs +11 -11
- package/dist/{api-Dmz40c2V.mjs → api-Cs7DAACP.mjs} +12 -12
- package/dist/{api-Dmz40c2V.mjs.map → api-Cs7DAACP.mjs.map} +1 -1
- package/dist/{apply-CuuZG6op.mjs → apply-BWMV4Zmw.mjs} +16 -16
- package/dist/{apply-CuuZG6op.mjs.map → apply-BWMV4Zmw.mjs.map} +1 -1
- package/dist/astro/index.mjs +1 -1
- package/dist/astro/middleware/auth.mjs +2 -2
- package/dist/astro/middleware/redirect.mjs +5 -5
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +274 -91
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +3 -3
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +3 -3
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +3 -3
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +3 -3
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +4 -4
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +4 -4
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +4 -4
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +9 -9
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +9 -9
- package/dist/astro/routes/api/admin/bylines/index.mjs +9 -9
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +7 -7
- package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/bulk.mjs +6 -6
- package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/index.mjs +6 -6
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/index.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/updates.mjs +26 -26
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +26 -26
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +26 -26
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +3 -3
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +2 -2
- package/dist/astro/routes/api/admin/users/index.mjs +3 -3
- package/dist/astro/routes/api/auth/dev-bypass.mjs +4 -4
- package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
- package/dist/astro/routes/api/auth/invite/complete.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/index.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/register-options.mjs +3 -3
- package/dist/astro/routes/api/auth/logout.mjs +2 -2
- package/dist/astro/routes/api/auth/magic-link/send.mjs +4 -4
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
- package/dist/astro/routes/api/auth/me.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/options.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/verify.mjs +3 -3
- package/dist/astro/routes/api/auth/signup/complete.mjs +3 -3
- package/dist/astro/routes/api/auth/signup/request.mjs +4 -4
- package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +6 -6
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +9 -9
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/index.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/trash.mjs +4 -4
- package/dist/astro/routes/api/dashboard.mjs +7 -7
- package/dist/astro/routes/api/dev/emails.mjs +2 -2
- package/dist/astro/routes/api/import/probe.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/execute.mjs +8 -8
- package/dist/astro/routes/api/import/wordpress/media.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +6 -6
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +5 -5
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +6 -6
- package/dist/astro/routes/api/manifest.mjs +3 -3
- package/dist/astro/routes/api/mcp.mjs +26 -26
- package/dist/astro/routes/api/media/_id_/confirm.mjs +4 -4
- package/dist/astro/routes/api/media/_id_.mjs +4 -4
- package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
- package/dist/astro/routes/api/media/providers/index.mjs +3 -3
- package/dist/astro/routes/api/media/upload-url.mjs +4 -4
- package/dist/astro/routes/api/media.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/items.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/translations.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_.mjs +5 -5
- package/dist/astro/routes/api/menus/index.mjs +5 -5
- package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
- package/dist/astro/routes/api/oauth/device/code.mjs +4 -4
- package/dist/astro/routes/api/oauth/device/token.mjs +4 -4
- package/dist/astro/routes/api/oauth/register.mjs +2 -2
- package/dist/astro/routes/api/oauth/token/refresh.mjs +3 -3
- package/dist/astro/routes/api/oauth/token/revoke.mjs +3 -3
- package/dist/astro/routes/api/oauth/token.mjs +2 -2
- package/dist/astro/routes/api/openapi.json.mjs +2 -2
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
- package/dist/astro/routes/api/redirects/404s/index.mjs +7 -7
- package/dist/astro/routes/api/redirects/404s/summary.mjs +7 -7
- package/dist/astro/routes/api/redirects/_id_.mjs +8 -8
- package/dist/astro/routes/api/redirects/index.mjs +8 -8
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +26 -26
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +26 -26
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +26 -26
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +26 -26
- package/dist/astro/routes/api/schema/collections/index.mjs +26 -26
- package/dist/astro/routes/api/schema/index.mjs +7 -7
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +26 -26
- package/dist/astro/routes/api/schema/orphans/index.mjs +26 -26
- package/dist/astro/routes/api/search/enable.mjs +8 -8
- package/dist/astro/routes/api/search/index.mjs +7 -7
- package/dist/astro/routes/api/search/rebuild.mjs +8 -8
- package/dist/astro/routes/api/search/stats.mjs +7 -7
- package/dist/astro/routes/api/search/suggest.mjs +7 -7
- package/dist/astro/routes/api/sections/_slug_.mjs +7 -7
- package/dist/astro/routes/api/sections/index.mjs +7 -7
- package/dist/astro/routes/api/settings/email.mjs +4 -4
- package/dist/astro/routes/api/settings.mjs +9 -9
- package/dist/astro/routes/api/setup/admin-verify.mjs +3 -3
- package/dist/astro/routes/api/setup/admin.mjs +3 -3
- package/dist/astro/routes/api/setup/dev-bypass.mjs +16 -16
- package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
- package/dist/astro/routes/api/setup/index.mjs +17 -17
- package/dist/astro/routes/api/setup/status.mjs +3 -3
- package/dist/astro/routes/api/snapshot.mjs +3 -3
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +9 -9
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +9 -9
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +9 -9
- package/dist/astro/routes/api/taxonomies/index.mjs +9 -9
- package/dist/astro/routes/api/themes/preview.mjs +3 -3
- package/dist/astro/routes/api/typegen.mjs +5 -5
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +4 -4
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +7 -7
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +7 -7
- package/dist/astro/routes/api/widget-areas/_name_.mjs +6 -6
- package/dist/astro/routes/api/widget-areas/index.mjs +7 -7
- package/dist/astro/routes/api/widget-components.mjs +2 -2
- package/dist/astro/routes/robots.txt.mjs +5 -5
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +5 -5
- package/dist/astro/routes/sitemap.xml.mjs +5 -5
- package/dist/{authorize-_wWM_44T.mjs → authorize-CotM4Yiu.mjs} +2 -2
- package/dist/{authorize-_wWM_44T.mjs.map → authorize-CotM4Yiu.mjs.map} +1 -1
- package/dist/{byline-BrIVWLm-.mjs → byline-CWQ9aSoz.mjs} +4 -4
- package/dist/{byline-BrIVWLm-.mjs.map → byline-CWQ9aSoz.mjs.map} +1 -1
- package/dist/{bylines-C_POWmGT.mjs → bylines-BJSva1Un.mjs} +4 -4
- package/dist/{bylines-C_POWmGT.mjs.map → bylines-BJSva1Un.mjs.map} +1 -1
- package/dist/{bylines-sqExMElV.mjs → bylines-LJMgENMI.mjs} +3 -3
- package/dist/{bylines-sqExMElV.mjs.map → bylines-LJMgENMI.mjs.map} +1 -1
- package/dist/{cache-wsDkA8ru.mjs → cache-lZL7SgVb.mjs} +2 -2
- package/dist/{cache-wsDkA8ru.mjs.map → cache-lZL7SgVb.mjs.map} +1 -1
- package/dist/{chunks-BAYkM-CF.mjs → chunks-BU-vP9Dh.mjs} +2 -2
- package/dist/{chunks-BAYkM-CF.mjs.map → chunks-BU-vP9Dh.mjs.map} +1 -1
- package/dist/cli/index.mjs +14 -14
- package/dist/{comment-Cd29aktf.mjs → comment-C4jVbCM8.mjs} +2 -2
- package/dist/{comment-Cd29aktf.mjs.map → comment-C4jVbCM8.mjs.map} +1 -1
- package/dist/{comments-B7ufhkxN.mjs → comments-BTAbC0Ek.mjs} +3 -3
- package/dist/{comments-B7ufhkxN.mjs.map → comments-BTAbC0Ek.mjs.map} +1 -1
- package/dist/{content-BbqKo3Kc.mjs → content-CyqOmOzm.mjs} +3 -3
- package/dist/{content-BbqKo3Kc.mjs.map → content-CyqOmOzm.mjs.map} +1 -1
- package/dist/{context-BsF1rhoI.mjs → context-DZ7bEh5-.mjs} +8 -8
- package/dist/{context-BsF1rhoI.mjs.map → context-DZ7bEh5-.mjs.map} +1 -1
- package/dist/{dashboard-BwIX9r-X.mjs → dashboard-B5WQpNTP.mjs} +4 -4
- package/dist/{dashboard-BwIX9r-X.mjs.map → dashboard-B5WQpNTP.mjs.map} +1 -1
- package/dist/db/index.mjs +2 -2
- package/dist/{dialect-helpers-BKCvISIQ.mjs → dialect-helpers-DRI5pyY3.mjs} +3 -3
- package/dist/dialect-helpers-DRI5pyY3.mjs.map +1 -0
- package/dist/{error-npZWBSb7.mjs → error-DJOsMVSt.mjs} +2 -2
- package/dist/{error-npZWBSb7.mjs.map → error-DJOsMVSt.mjs.map} +1 -1
- package/dist/{fts-manager-DmUAk-kQ.mjs → fts-manager-DR1ERA0c.mjs} +3 -3
- package/dist/{fts-manager-DmUAk-kQ.mjs.map → fts-manager-DR1ERA0c.mjs.map} +1 -1
- package/dist/index-CjKdMZ3U.d.mts.map +1 -1
- package/dist/index.mjs +35 -35
- package/dist/{load-DsoLq7ex.mjs → load-6ZrRhepW.mjs} +2 -2
- package/dist/{load-DsoLq7ex.mjs.map → load-6ZrRhepW.mjs.map} +1 -1
- package/dist/{loader-CJ6lWO0d.mjs → loader-Dyx8dhFV.mjs} +4 -4
- package/dist/{loader-CJ6lWO0d.mjs.map → loader-Dyx8dhFV.mjs.map} +1 -1
- package/dist/media/local-runtime.mjs +5 -5
- package/dist/{media-jk_HzzOl.mjs → media-C-oovGCG.mjs} +2 -2
- package/dist/{media-jk_HzzOl.mjs.map → media-C-oovGCG.mjs.map} +1 -1
- package/dist/{menus-CyMO6GBx.mjs → menus-BKkxXCmd.mjs} +30 -11
- package/dist/menus-BKkxXCmd.mjs.map +1 -0
- package/dist/{menus-B-5-3aon.mjs → menus-DugoYwTX.mjs} +2 -2
- package/dist/{menus-B-5-3aon.mjs.map → menus-DugoYwTX.mjs.map} +1 -1
- package/dist/{parse-4zO5Y2DL.mjs → parse-BBkFmLVr.mjs} +2 -2
- package/dist/{parse-4zO5Y2DL.mjs.map → parse-BBkFmLVr.mjs.map} +1 -1
- package/dist/{query-Bt52mHXp.mjs → query-Ctlq1aOk.mjs} +10 -10
- package/dist/{query-Bt52mHXp.mjs.map → query-Ctlq1aOk.mjs.map} +1 -1
- package/dist/{rate-limit-D6VQqBk_.mjs → rate-limit-CH6W6ikK.mjs} +2 -2
- package/dist/{rate-limit-D6VQqBk_.mjs.map → rate-limit-CH6W6ikK.mjs.map} +1 -1
- package/dist/{redirect-BZUJltlj.mjs → redirect-C6tJA7tk.mjs} +3 -3
- package/dist/{redirect-BZUJltlj.mjs.map → redirect-C6tJA7tk.mjs.map} +1 -1
- package/dist/{redirects-DnYuqsEf.mjs → redirects-CacE9eQa.mjs} +3 -3
- package/dist/{redirects-DnYuqsEf.mjs.map → redirects-CacE9eQa.mjs.map} +1 -1
- package/dist/{registry-Dn6gsx3L.mjs → registry-CIDxZbhh.mjs} +5 -5
- package/dist/{registry-Dn6gsx3L.mjs.map → registry-CIDxZbhh.mjs.map} +1 -1
- package/dist/runner-DM1yR5qd.d.mts.map +1 -1
- package/dist/{runner-eAgyIkeg.mjs → runner-pt6Wl-l-.mjs} +11 -6
- package/dist/runner-pt6Wl-l-.mjs.map +1 -0
- package/dist/runtime.mjs +3 -3
- package/dist/{schema--mYZX4D7.mjs → schema-B4tk0HAG.mjs} +4 -4
- package/dist/{schema--mYZX4D7.mjs.map → schema-B4tk0HAG.mjs.map} +1 -1
- package/dist/{search-C6U_NvZI.mjs → search-f-fNfwab.mjs} +4 -4
- package/dist/{search-C6U_NvZI.mjs.map → search-f-fNfwab.mjs.map} +1 -1
- package/dist/{sections-Ba-rJLKb.mjs → sections-biElLfT9.mjs} +3 -3
- package/dist/{sections-Ba-rJLKb.mjs.map → sections-biElLfT9.mjs.map} +1 -1
- package/dist/seed/index.mjs +14 -14
- package/dist/seo/index.mjs +1 -0
- package/dist/seo/index.mjs.map +1 -1
- package/dist/{seo-BTzb5ksq.mjs → seo-BR39kvTF.mjs} +2 -2
- package/dist/{seo-BTzb5ksq.mjs.map → seo-BR39kvTF.mjs.map} +1 -1
- package/dist/{service-Cn-kIfZn.mjs → service-BhR2acnc.mjs} +2 -2
- package/dist/{service-Cn-kIfZn.mjs.map → service-BhR2acnc.mjs.map} +1 -1
- package/dist/{settings-C65OSm41.mjs → settings-D_NJvjgN.mjs} +3 -3
- package/dist/{settings-C65OSm41.mjs.map → settings-D_NJvjgN.mjs.map} +1 -1
- package/dist/{settings-ChlQbwU0.mjs → settings-b5zW1R1T.mjs} +3 -3
- package/dist/{settings-ChlQbwU0.mjs.map → settings-b5zW1R1T.mjs.map} +1 -1
- package/dist/{taxonomies-ByLlXrv5.mjs → taxonomies-Crtzy4MT.mjs} +8 -7
- package/dist/taxonomies-Crtzy4MT.mjs.map +1 -0
- package/dist/{taxonomies-CbO6v7EE.mjs → taxonomies-Mhn9rjTQ.mjs} +4 -4
- package/dist/{taxonomies-CbO6v7EE.mjs.map → taxonomies-Mhn9rjTQ.mjs.map} +1 -1
- package/dist/{taxonomy-BBK-UAEo.mjs → taxonomy-DTZrIQpi.mjs} +3 -3
- package/dist/{taxonomy-BBK-UAEo.mjs.map → taxonomy-DTZrIQpi.mjs.map} +1 -1
- package/dist/{types-SF1DwGf2.mjs → types-K3MDsxpy.mjs} +2 -2
- package/dist/{types-SF1DwGf2.mjs.map → types-K3MDsxpy.mjs.map} +1 -1
- package/dist/{user-X4rtyO4Y.mjs → user-DzEUl5zA.mjs} +2 -2
- package/dist/{user-X4rtyO4Y.mjs.map → user-DzEUl5zA.mjs.map} +1 -1
- package/dist/{validate-DactmcJG.mjs → validate-JCXcsqiY.mjs} +2 -2
- package/dist/{validate-DactmcJG.mjs.map → validate-JCXcsqiY.mjs.map} +1 -1
- package/dist/{validation-BYA4i85b.mjs → validation-Bq-VyKJg.mjs} +6 -6
- package/dist/{validation-BYA4i85b.mjs.map → validation-Bq-VyKJg.mjs.map} +1 -1
- package/dist/version-CnS-Cr8A.mjs +7 -0
- package/dist/{version-CWbvq9LG.mjs.map → version-CnS-Cr8A.mjs.map} +1 -1
- package/dist/{widgets-DG-1jxnz.mjs → widgets-Bap1eS1X.mjs} +2 -2
- package/dist/{widgets-DG-1jxnz.mjs.map → widgets-Bap1eS1X.mjs.map} +1 -1
- package/dist/{zod-generator-BNAObjSt.mjs → zod-generator-BSDpkqSH.mjs} +4 -3
- package/dist/zod-generator-BSDpkqSH.mjs.map +1 -0
- package/package.json +7 -7
- package/src/astro/middleware/stream-end-metrics.ts +96 -0
- package/src/astro/middleware.ts +114 -40
- package/src/components/EmDashImage.astro +1 -0
- package/src/database/dialect-helpers.ts +8 -2
- package/src/database/migrations/019_i18n.ts +2 -2
- package/src/database/migrations/runner.ts +7 -2
- package/src/emdash-runtime.ts +177 -126
- package/src/menus/index.ts +27 -9
- package/src/plugins/hooks.ts +35 -6
- package/src/plugins/manager.ts +1 -0
- package/src/schema/zod-generator.ts +6 -2
- package/src/seo/index.ts +10 -1
- package/src/taxonomies/index.ts +12 -8
- package/src/utils/init-lock.ts +143 -0
- package/dist/dialect-helpers-BKCvISIQ.mjs.map +0 -1
- package/dist/menus-CyMO6GBx.mjs.map +0 -1
- package/dist/runner-eAgyIkeg.mjs.map +0 -1
- package/dist/taxonomies-ByLlXrv5.mjs.map +0 -1
- package/dist/version-CWbvq9LG.mjs +0 -7
- package/dist/zod-generator-BNAObjSt.mjs.map +0 -1
package/src/emdash-runtime.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { getAuthMode } from "./auth/mode.js";
|
|
|
22
22
|
import { getTrustedProxyHeaders } from "./auth/trusted-proxy.js";
|
|
23
23
|
import { isSqlite } from "./database/dialect-helpers.js";
|
|
24
24
|
import { kyselyLogOption } from "./database/instrumentation.js";
|
|
25
|
-
import { runMigrations } from "./database/migrations/runner.js";
|
|
25
|
+
import { MIGRATION_RACE_WAIT_MS, runMigrations } from "./database/migrations/runner.js";
|
|
26
26
|
import { RevisionRepository } from "./database/repositories/revision.js";
|
|
27
27
|
import type { ContentItem as ContentItemInternal } from "./database/repositories/types.js";
|
|
28
28
|
import { validateIdentifier } from "./database/validate.js";
|
|
@@ -41,6 +41,7 @@ import type {
|
|
|
41
41
|
} from "./plugins/types.js";
|
|
42
42
|
import type { FieldType } from "./schema/types.js";
|
|
43
43
|
import { hashString } from "./utils/hash.js";
|
|
44
|
+
import { createInitLock, type InitLock, initWithLock } from "./utils/init-lock.js";
|
|
44
45
|
import { COMMIT, VERSION } from "./version.js";
|
|
45
46
|
|
|
46
47
|
const LEADING_SLASH_PATTERN = /^\//;
|
|
@@ -310,9 +311,38 @@ function contentItemToRecord(item: ContentItemInternal): Record<string, unknown>
|
|
|
310
311
|
return { ...item };
|
|
311
312
|
}
|
|
312
313
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Db init lock reclaim deadline. Derived from the migration race wait so
|
|
316
|
+
* they can't drift apart: a healthy init can legitimately block for the
|
|
317
|
+
* full MIGRATION_RACE_WAIT_MS inside waitForConcurrentMigrator, plus cold
|
|
318
|
+
* connect and migrator work, before it should be presumed dead. The outer
|
|
319
|
+
* runtime init lock (middleware.ts) must use a strictly larger deadline —
|
|
320
|
+
* it wraps create() → getDatabase() → this lock, and equal deadlines would
|
|
321
|
+
* let the outer reclaim while the inner is legitimately still working.
|
|
322
|
+
*/
|
|
323
|
+
export const DB_INIT_DEADLINE_MS = MIGRATION_RACE_WAIT_MS + 20_000;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Db cache + its init lock live on globalThis behind a Symbol: the bundler
|
|
327
|
+
* can duplicate this module across SSR chunks (same reasoning as
|
|
328
|
+
* request-cache.ts), and a duplicated cache/lock would mean concurrent
|
|
329
|
+
* independent db inits — and duplicate migrators — per isolate.
|
|
330
|
+
*/
|
|
331
|
+
const DB_HOLDER_KEY = Symbol.for("emdash:db-cache");
|
|
332
|
+
interface DbHolder {
|
|
333
|
+
cache: Map<string, Kysely<Database>>;
|
|
334
|
+
lock: InitLock;
|
|
335
|
+
}
|
|
336
|
+
const globalSymbolStore = globalThis as Record<symbol, unknown>;
|
|
337
|
+
function getDbHolder(): DbHolder {
|
|
338
|
+
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis symbol slot, written only below
|
|
339
|
+
let holder = globalSymbolStore[DB_HOLDER_KEY] as DbHolder | undefined;
|
|
340
|
+
if (!holder) {
|
|
341
|
+
holder = { cache: new Map<string, Kysely<Database>>(), lock: createInitLock() };
|
|
342
|
+
globalSymbolStore[DB_HOLDER_KEY] = holder;
|
|
343
|
+
}
|
|
344
|
+
return holder;
|
|
345
|
+
}
|
|
316
346
|
const storageCache = new Map<string, Storage>();
|
|
317
347
|
const sandboxedPluginCache = new Map<string, SandboxedPluginInstance>();
|
|
318
348
|
/**
|
|
@@ -887,19 +917,45 @@ export class EmDashRuntime {
|
|
|
887
917
|
// Initialize storage (sync)
|
|
888
918
|
const storage = EmDashRuntime.getStorage(deps);
|
|
889
919
|
|
|
890
|
-
// Fetch plugin states
|
|
920
|
+
// Fetch plugin states and site info concurrently — independent reads
|
|
921
|
+
// against different tables (_plugin_state vs options), so they share
|
|
922
|
+
// one round-trip window instead of paying two sequential ones. Each
|
|
923
|
+
// phase() wrapper still records that phase's own duration, and each
|
|
924
|
+
// body keeps its own non-fatal catch.
|
|
891
925
|
let pluginStates: Map<string, string> = new Map();
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
926
|
+
let siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;
|
|
927
|
+
await Promise.all([
|
|
928
|
+
// Fetch plugin states from database
|
|
929
|
+
phase("rt.plugins", "Plugin states", async () => {
|
|
930
|
+
try {
|
|
931
|
+
const states = await db
|
|
932
|
+
.selectFrom("_plugin_state")
|
|
933
|
+
.select(["plugin_id", "status"])
|
|
934
|
+
.execute();
|
|
935
|
+
pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
|
|
936
|
+
} catch {
|
|
937
|
+
// Plugin state table may not exist yet
|
|
938
|
+
}
|
|
939
|
+
}),
|
|
940
|
+
// Load site info for plugin context extensions (1 batch query instead of 3)
|
|
941
|
+
phase("rt.site", "Site info options", async () => {
|
|
942
|
+
try {
|
|
943
|
+
const optionsRepo = new OptionsRepository(db);
|
|
944
|
+
const siteOpts = await optionsRepo.getMany<string>([
|
|
945
|
+
"emdash:site_title",
|
|
946
|
+
"emdash:site_url",
|
|
947
|
+
"emdash:locale",
|
|
948
|
+
]);
|
|
949
|
+
siteInfo = {
|
|
950
|
+
siteName: siteOpts.get("emdash:site_title") ?? undefined,
|
|
951
|
+
siteUrl: siteOpts.get("emdash:site_url") ?? undefined,
|
|
952
|
+
locale: siteOpts.get("emdash:locale") ?? undefined,
|
|
953
|
+
};
|
|
954
|
+
} catch {
|
|
955
|
+
// Options table may not exist yet (pre-setup)
|
|
956
|
+
}
|
|
957
|
+
}),
|
|
958
|
+
]);
|
|
903
959
|
|
|
904
960
|
// Build set of enabled plugins
|
|
905
961
|
const enabledPlugins = new Set<string>();
|
|
@@ -910,26 +966,6 @@ export class EmDashRuntime {
|
|
|
910
966
|
}
|
|
911
967
|
}
|
|
912
968
|
|
|
913
|
-
// Load site info for plugin context extensions (1 batch query instead of 3)
|
|
914
|
-
let siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;
|
|
915
|
-
await phase("rt.site", "Site info options", async () => {
|
|
916
|
-
try {
|
|
917
|
-
const optionsRepo = new OptionsRepository(db);
|
|
918
|
-
const siteOpts = await optionsRepo.getMany<string>([
|
|
919
|
-
"emdash:site_title",
|
|
920
|
-
"emdash:site_url",
|
|
921
|
-
"emdash:locale",
|
|
922
|
-
]);
|
|
923
|
-
siteInfo = {
|
|
924
|
-
siteName: siteOpts.get("emdash:site_title") ?? undefined,
|
|
925
|
-
siteUrl: siteOpts.get("emdash:site_url") ?? undefined,
|
|
926
|
-
locale: siteOpts.get("emdash:locale") ?? undefined,
|
|
927
|
-
};
|
|
928
|
-
} catch {
|
|
929
|
-
// Options table may not exist yet (pre-setup)
|
|
930
|
-
}
|
|
931
|
-
});
|
|
932
|
-
|
|
933
969
|
// Build the full list of pipeline-eligible plugins: all configured
|
|
934
970
|
// plugins (regardless of current enabled status) plus built-in plugins.
|
|
935
971
|
// rebuildHookPipeline() filters this to only enabled plugins.
|
|
@@ -1050,32 +1086,43 @@ export class EmDashRuntime {
|
|
|
1050
1086
|
EmDashRuntime.loadSandboxedPlugins(deps, db, storage),
|
|
1051
1087
|
);
|
|
1052
1088
|
|
|
1053
|
-
// Cold-start: load marketplace-installed plugins from
|
|
1054
|
-
// the sandbox runner.
|
|
1089
|
+
// Cold-start: load marketplace- and registry-installed plugins from
|
|
1090
|
+
// site R2 via the sandbox runner. The two tiers only depend on the
|
|
1091
|
+
// sandbox phase above, not on each other, so when both are enabled
|
|
1092
|
+
// they run concurrently instead of paying two sequential loads.
|
|
1093
|
+
// In bypass mode marketplace plugins were already handled above.
|
|
1094
|
+
const installedTierPhases: Promise<void>[] = [];
|
|
1055
1095
|
if (deps.config.marketplace && storage && !deps.sandboxBypassed) {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1096
|
+
installedTierPhases.push(
|
|
1097
|
+
phase("rt.market", "Marketplace plugins", () =>
|
|
1098
|
+
EmDashRuntime.loadInstalledSandboxedPlugins(
|
|
1099
|
+
"marketplace",
|
|
1100
|
+
db,
|
|
1101
|
+
storage,
|
|
1102
|
+
deps,
|
|
1103
|
+
sandboxedPlugins,
|
|
1104
|
+
),
|
|
1063
1105
|
),
|
|
1064
1106
|
);
|
|
1065
1107
|
}
|
|
1066
1108
|
|
|
1067
1109
|
// Cold-start: load registry-installed plugins from site R2
|
|
1068
1110
|
if (deps.config.experimental?.registry && storage) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1111
|
+
installedTierPhases.push(
|
|
1112
|
+
phase("rt.registry", "Registry plugins", () =>
|
|
1113
|
+
EmDashRuntime.loadInstalledSandboxedPlugins(
|
|
1114
|
+
"registry",
|
|
1115
|
+
db,
|
|
1116
|
+
storage,
|
|
1117
|
+
deps,
|
|
1118
|
+
sandboxedPlugins,
|
|
1119
|
+
),
|
|
1076
1120
|
),
|
|
1077
1121
|
);
|
|
1078
1122
|
}
|
|
1123
|
+
if (installedTierPhases.length > 0) {
|
|
1124
|
+
await Promise.all(installedTierPhases);
|
|
1125
|
+
}
|
|
1079
1126
|
|
|
1080
1127
|
// Initialize media providers
|
|
1081
1128
|
const mediaProviders = new Map<string, MediaProvider>();
|
|
@@ -1270,83 +1317,86 @@ export class EmDashRuntime {
|
|
|
1270
1317
|
|
|
1271
1318
|
const cacheKey = dbConfig.entrypoint;
|
|
1272
1319
|
|
|
1273
|
-
//
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
//
|
|
1280
|
-
//
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
console.log("Auto-seeded default collections");
|
|
1320
|
+
// Waiters poll the cache rather than sharing the initializing request's
|
|
1321
|
+
// promise: if the request that owns the init is cancelled mid-await
|
|
1322
|
+
// (e.g. client disconnect during cold migrations), a shared promise
|
|
1323
|
+
// never settles — and the owner's `finally` that would clear it never
|
|
1324
|
+
// runs — deadlocking every later request in the isolate. Prevention:
|
|
1325
|
+
// the in-flight init is anchored via after()/waitUntil so a cancelled
|
|
1326
|
+
// owner's init still completes and populates the cache. Net: a stale
|
|
1327
|
+
// lock is reclaimed after a deadline.
|
|
1328
|
+
const holder = getDbHolder();
|
|
1329
|
+
return initWithLock(
|
|
1330
|
+
holder.lock,
|
|
1331
|
+
() => holder.cache.get(cacheKey),
|
|
1332
|
+
async (isCurrentClaim) => {
|
|
1333
|
+
const dialect = deps.createDialect(dbConfig.config);
|
|
1334
|
+
const db = new Kysely<Database>({ dialect, log: kyselyLogOption() });
|
|
1335
|
+
|
|
1336
|
+
await runMigrations(db);
|
|
1337
|
+
|
|
1338
|
+
// Note: legacy installs may carry a stray `emdash:manifest_cache`
|
|
1339
|
+
// row in the options table from versions that persisted a JSON
|
|
1340
|
+
// manifest. The runtime no longer reads or writes it. We do not
|
|
1341
|
+
// proactively delete it: the row is a few hundred bytes of dead
|
|
1342
|
+
// weight and is never on the read path, whereas a one-shot
|
|
1343
|
+
// cleanup-flag check costs an extra `options.get()` on every
|
|
1344
|
+
// isolate cold boot forever. Cheaper to leave it.
|
|
1345
|
+
|
|
1346
|
+
// Auto-seed schema if no collections exist and setup hasn't run.
|
|
1347
|
+
// This covers first-load on sites that skip the setup wizard.
|
|
1348
|
+
// Dev-bypass and the wizard apply seeds explicitly.
|
|
1349
|
+
try {
|
|
1350
|
+
const [collectionCount, setupOption] = await Promise.all([
|
|
1351
|
+
db
|
|
1352
|
+
.selectFrom("_emdash_collections")
|
|
1353
|
+
.select((eb) => eb.fn.countAll<number>().as("count"))
|
|
1354
|
+
.executeTakeFirstOrThrow(),
|
|
1355
|
+
db
|
|
1356
|
+
.selectFrom("options")
|
|
1357
|
+
.select("value")
|
|
1358
|
+
.where("name", "=", "emdash:setup_complete")
|
|
1359
|
+
.executeTakeFirst(),
|
|
1360
|
+
]);
|
|
1361
|
+
|
|
1362
|
+
const setupDone = (() => {
|
|
1363
|
+
try {
|
|
1364
|
+
return setupOption && JSON.parse(setupOption.value) === true;
|
|
1365
|
+
} catch {
|
|
1366
|
+
return false;
|
|
1367
|
+
}
|
|
1368
|
+
})();
|
|
1369
|
+
|
|
1370
|
+
if (collectionCount.count === 0 && !setupDone) {
|
|
1371
|
+
const { applySeed } = await import("./seed/apply.js");
|
|
1372
|
+
const { loadSeed } = await import("./seed/load.js");
|
|
1373
|
+
const { validateSeed } = await import("./seed/validate.js");
|
|
1374
|
+
|
|
1375
|
+
const seed = await loadSeed();
|
|
1376
|
+
const validation = validateSeed(seed);
|
|
1377
|
+
if (validation.valid) {
|
|
1378
|
+
await applySeed(db, seed, { onConflict: "skip" });
|
|
1379
|
+
console.log("Auto-seeded default collections");
|
|
1380
|
+
}
|
|
1335
1381
|
}
|
|
1382
|
+
} catch {
|
|
1383
|
+
// Tables may not exist yet. Non-fatal.
|
|
1336
1384
|
}
|
|
1337
|
-
} catch {
|
|
1338
|
-
// Tables may not exist yet. Non-fatal.
|
|
1339
|
-
}
|
|
1340
1385
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1386
|
+
// Publish only while still the current owner: a reclaimed slow
|
|
1387
|
+
// init must not flip the cached Kysely identity back after the
|
|
1388
|
+
// reclaimer has published its own. The unpublished instance is
|
|
1389
|
+
// still returned and fully valid for the request that built it.
|
|
1390
|
+
if (isCurrentClaim()) {
|
|
1391
|
+
holder.cache.set(cacheKey, db);
|
|
1392
|
+
}
|
|
1393
|
+
return db;
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
deadlineMs: DB_INIT_DEADLINE_MS,
|
|
1397
|
+
anchor: (promise) => after(() => promise),
|
|
1398
|
+
},
|
|
1399
|
+
);
|
|
1350
1400
|
}
|
|
1351
1401
|
|
|
1352
1402
|
/**
|
|
@@ -1778,6 +1828,7 @@ export class EmDashRuntime {
|
|
|
1778
1828
|
pipeline,
|
|
1779
1829
|
isActive: () => true,
|
|
1780
1830
|
getOption: (key) => optionsRepo.get<string>(key),
|
|
1831
|
+
getOptions: (keys) => optionsRepo.getMany<string>(keys),
|
|
1781
1832
|
setOption: (key, value) => optionsRepo.set(key, value),
|
|
1782
1833
|
deleteOption: async (key) => {
|
|
1783
1834
|
await optionsRepo.delete(key);
|
package/src/menus/index.ts
CHANGED
|
@@ -136,15 +136,10 @@ async function buildMenuTree(
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
const urlPatterns =
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.select(["slug", "url_pattern"])
|
|
144
|
-
.where("slug", "in", [...collectionSlugs])
|
|
145
|
-
.execute();
|
|
146
|
-
for (const row of rows) urlPatterns.set(row.slug, row.url_pattern);
|
|
147
|
-
}
|
|
139
|
+
const urlPatterns =
|
|
140
|
+
collectionSlugs.size > 0
|
|
141
|
+
? await getCollectionUrlPatterns(db, collectionSlugs)
|
|
142
|
+
: new Map<string, string | null>();
|
|
148
143
|
|
|
149
144
|
const resolvedItems = await Promise.all(
|
|
150
145
|
items.map((item) => resolveMenuItem(item, db, urlPatterns, locale)),
|
|
@@ -173,6 +168,29 @@ async function buildMenuTree(
|
|
|
173
168
|
return rootItems;
|
|
174
169
|
}
|
|
175
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Look up the `url_pattern` for a set of collection slugs, request-cached so
|
|
173
|
+
* a page rendering several menus (header, footer, ...) only pays for the
|
|
174
|
+
* lookup once per distinct slug set. Callers must treat the returned map as
|
|
175
|
+
* read-only — it is shared across cache hits within the request.
|
|
176
|
+
*/
|
|
177
|
+
function getCollectionUrlPatterns(
|
|
178
|
+
db: Kysely<Database>,
|
|
179
|
+
collectionSlugs: Set<string>,
|
|
180
|
+
): Promise<Map<string, string | null>> {
|
|
181
|
+
const key = `menu-collection-patterns:${[...collectionSlugs].toSorted().join(",")}`;
|
|
182
|
+
return requestCached(key, async () => {
|
|
183
|
+
const rows = await db
|
|
184
|
+
.selectFrom("_emdash_collections")
|
|
185
|
+
.select(["slug", "url_pattern"])
|
|
186
|
+
.where("slug", "in", [...collectionSlugs])
|
|
187
|
+
.execute();
|
|
188
|
+
const urlPatterns = new Map<string, string | null>();
|
|
189
|
+
for (const row of rows) urlPatterns.set(row.slug, row.url_pattern);
|
|
190
|
+
return urlPatterns;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
176
194
|
/**
|
|
177
195
|
* Resolve a single menu item's URL. `reference_id` is a translation_group
|
|
178
196
|
* (migration 036 remapped all existing references); we join it against
|
package/src/plugins/hooks.ts
CHANGED
|
@@ -1297,6 +1297,13 @@ export interface ExclusiveHookResolutionOptions {
|
|
|
1297
1297
|
isActive: (pluginId: string) => boolean;
|
|
1298
1298
|
/** Read an option value from persistent storage. */
|
|
1299
1299
|
getOption: (key: string) => Promise<string | null>;
|
|
1300
|
+
/**
|
|
1301
|
+
* Batch-read option values for many keys in a single round trip.
|
|
1302
|
+
* When provided, resolution reads all current selections through this
|
|
1303
|
+
* instead of one getOption() call per hook. Keys absent from the
|
|
1304
|
+
* returned map are treated as unset.
|
|
1305
|
+
*/
|
|
1306
|
+
getOptions?: (keys: string[]) => Promise<ReadonlyMap<string, string>>;
|
|
1300
1307
|
/** Write an option value to persistent storage. */
|
|
1301
1308
|
setOption: (key: string, value: string) => Promise<void>;
|
|
1302
1309
|
/** Delete an option from persistent storage. */
|
|
@@ -1322,8 +1329,26 @@ const EXCLUSIVE_HOOK_KEY_PREFIX = "emdash:exclusive_hook:";
|
|
|
1322
1329
|
* 5. If multiple providers and no hint → leave unselected (admin must choose).
|
|
1323
1330
|
*/
|
|
1324
1331
|
export async function resolveExclusiveHooks(opts: ExclusiveHookResolutionOptions): Promise<void> {
|
|
1325
|
-
const { pipeline, isActive, getOption, setOption, deleteOption, preferredHints } =
|
|
1332
|
+
const { pipeline, isActive, getOption, getOptions, setOption, deleteOption, preferredHints } =
|
|
1333
|
+
opts;
|
|
1326
1334
|
const exclusiveHookNames = pipeline.getRegisteredExclusiveHooks();
|
|
1335
|
+
if (exclusiveHookNames.length === 0) return;
|
|
1336
|
+
|
|
1337
|
+
// Batch-read current selections in one round trip when the caller
|
|
1338
|
+
// provides a batch reader (1 query instead of N sequential gets).
|
|
1339
|
+
let batchedSelections: ReadonlyMap<string, string> | undefined;
|
|
1340
|
+
if (getOptions) {
|
|
1341
|
+
try {
|
|
1342
|
+
batchedSelections = await getOptions(
|
|
1343
|
+
exclusiveHookNames.map((hookName) => `${EXCLUSIVE_HOOK_KEY_PREFIX}${hookName}`),
|
|
1344
|
+
);
|
|
1345
|
+
} catch {
|
|
1346
|
+
// Options table may not be ready. Matches the per-key tolerance
|
|
1347
|
+
// below: every hook's read would fail, so resolution is skipped
|
|
1348
|
+
// entirely without touching any selection.
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1327
1352
|
|
|
1328
1353
|
for (const hookName of exclusiveHookNames) {
|
|
1329
1354
|
const providers = pipeline.getExclusiveHookProviders(hookName);
|
|
@@ -1333,11 +1358,15 @@ export async function resolveExclusiveHooks(opts: ExclusiveHookResolutionOptions
|
|
|
1333
1358
|
|
|
1334
1359
|
const key = `${EXCLUSIVE_HOOK_KEY_PREFIX}${hookName}`;
|
|
1335
1360
|
let currentSelection: string | null = null;
|
|
1336
|
-
|
|
1337
|
-
currentSelection =
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1361
|
+
if (batchedSelections) {
|
|
1362
|
+
currentSelection = batchedSelections.get(key) ?? null;
|
|
1363
|
+
} else {
|
|
1364
|
+
try {
|
|
1365
|
+
currentSelection = await getOption(key);
|
|
1366
|
+
} catch {
|
|
1367
|
+
// Options table may not be ready
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1341
1370
|
}
|
|
1342
1371
|
|
|
1343
1372
|
// If selection exists and the plugin is still active → keep it
|
package/src/plugins/manager.ts
CHANGED
|
@@ -544,6 +544,7 @@ export class PluginManager {
|
|
|
544
544
|
pipeline: this.hookPipeline!,
|
|
545
545
|
isActive: (pluginId) => this.isActive(pluginId),
|
|
546
546
|
getOption: (key) => optionsRepo.get<string>(key),
|
|
547
|
+
getOptions: (keys) => optionsRepo.getMany<string>(keys),
|
|
547
548
|
setOption: (key, value) => optionsRepo.set(key, value),
|
|
548
549
|
deleteOption: async (key) => {
|
|
549
550
|
await optionsRepo.delete(key);
|
|
@@ -288,6 +288,9 @@ export function generateTypeScript(collection: CollectionWithFields): string {
|
|
|
288
288
|
lines.push(` publishedAt: Date | null;`);
|
|
289
289
|
// Bylines are eagerly loaded by getEmDashCollection/getEmDashEntry
|
|
290
290
|
lines.push(` bylines?: ContentBylineCredit[];`);
|
|
291
|
+
// Taxonomy terms are eagerly loaded by getEmDashCollection/getEmDashEntry,
|
|
292
|
+
// keyed by taxonomy name (e.g. data.terms?.tag)
|
|
293
|
+
lines.push(` terms?: Record<string, TaxonomyTerm[]>;`);
|
|
291
294
|
lines.push(`}`);
|
|
292
295
|
|
|
293
296
|
return lines.join("\n");
|
|
@@ -312,8 +315,9 @@ export function generateTypesFile(collections: CollectionWithFields[]): string {
|
|
|
312
315
|
c.fields.some((f) => f.type === "portableText"),
|
|
313
316
|
);
|
|
314
317
|
|
|
315
|
-
// Build imports - ContentBylineCredit
|
|
316
|
-
|
|
318
|
+
// Build imports - ContentBylineCredit and TaxonomyTerm are always needed
|
|
319
|
+
// for the hydrated bylines/terms fields
|
|
320
|
+
const imports = ["ContentBylineCredit", "TaxonomyTerm"];
|
|
317
321
|
if (needsPortableText) {
|
|
318
322
|
imports.push("PortableTextBlock");
|
|
319
323
|
}
|
package/src/seo/index.ts
CHANGED
|
@@ -170,7 +170,16 @@ function buildMediaUrl(imageRef: string, siteUrl?: string): string {
|
|
|
170
170
|
return imageRef;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
//
|
|
173
|
+
// Root-relative path — the CMS SEO panel stores seo_image as
|
|
174
|
+
// "/_emdash/api/media/file/01KS....svg" (already includes the API
|
|
175
|
+
// prefix). Without this branch we'd re-prefix and produce
|
|
176
|
+
// "${siteUrl}/_emdash/api/media/file//_emdash/api/media/file/<id>"
|
|
177
|
+
// which 404s and breaks <meta property="og:image">.
|
|
178
|
+
if (imageRef.startsWith("/")) {
|
|
179
|
+
return siteUrl ? `${siteUrl.replace(TRAILING_SLASH_RE, "")}${imageRef}` : imageRef;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Bare media_id — build the full media API path
|
|
174
183
|
const mediaPath = `/_emdash/api/media/file/${imageRef}`;
|
|
175
184
|
if (siteUrl) {
|
|
176
185
|
return `${siteUrl.replace(TRAILING_SLASH_RE, "")}${mediaPath}`;
|
package/src/taxonomies/index.ts
CHANGED
|
@@ -189,13 +189,6 @@ export async function getTerm(
|
|
|
189
189
|
|
|
190
190
|
if (!row) return null;
|
|
191
191
|
|
|
192
|
-
const countResult = await db
|
|
193
|
-
.selectFrom("content_taxonomies")
|
|
194
|
-
.select((eb) => eb.fn.count<number>("entry_id").as("count"))
|
|
195
|
-
.where("taxonomy_id", "=", row.translation_group ?? row.id)
|
|
196
|
-
.executeTakeFirst();
|
|
197
|
-
const count = countResult?.count ?? 0;
|
|
198
|
-
|
|
199
192
|
let childrenQuery = db
|
|
200
193
|
.selectFrom("taxonomies")
|
|
201
194
|
.selectAll()
|
|
@@ -203,7 +196,18 @@ export async function getTerm(
|
|
|
203
196
|
.orderBy("label", "asc");
|
|
204
197
|
const termLocale = row.locale;
|
|
205
198
|
if (termLocale) childrenQuery = childrenQuery.where("locale", "=", termLocale);
|
|
206
|
-
|
|
199
|
+
|
|
200
|
+
// The usage-count and children queries both depend only on the term row,
|
|
201
|
+
// so run them concurrently to save a round trip on remote databases.
|
|
202
|
+
const [countResult, childRows] = await Promise.all([
|
|
203
|
+
db
|
|
204
|
+
.selectFrom("content_taxonomies")
|
|
205
|
+
.select((eb) => eb.fn.count<number>("entry_id").as("count"))
|
|
206
|
+
.where("taxonomy_id", "=", row.translation_group ?? row.id)
|
|
207
|
+
.executeTakeFirst(),
|
|
208
|
+
childrenQuery.execute(),
|
|
209
|
+
]);
|
|
210
|
+
const count = countResult?.count ?? 0;
|
|
207
211
|
|
|
208
212
|
const children = childRows.map<TaxonomyTerm>((child) => ({
|
|
209
213
|
id: child.id,
|