emdash 0.26.0 → 0.28.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/{adapters-DAAjOID4.d.mts → adapters-hUy8vOB-.d.mts} +1 -1
- package/dist/{adapters-DAAjOID4.d.mts.map → adapters-hUy8vOB-.d.mts.map} +1 -1
- package/dist/{allowed-origins-D5FxMUo8.mjs → allowed-origins-CRsJ6BPM.mjs} +2 -2
- package/dist/{allowed-origins-D5FxMUo8.mjs.map → allowed-origins-CRsJ6BPM.mjs.map} +1 -1
- package/dist/api/route-utils.d.mts +3 -3
- package/dist/api/route-utils.mjs +17 -16
- package/dist/api/route-utils.mjs.map +1 -1
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/api/schemas/index.mjs +3 -3
- package/dist/{api-BMfveMoB.mjs → api-BFURZn1i.mjs} +104 -38
- package/dist/api-BFURZn1i.mjs.map +1 -0
- package/dist/{api-tokens-D6ppjIVi.mjs → api-tokens-DvqeZmVq.mjs} +2 -2
- package/dist/{api-tokens-D6ppjIVi.mjs.map → api-tokens-DvqeZmVq.mjs.map} +1 -1
- package/dist/{apply--IVNSdXn.mjs → apply-Bu-8nYHL.mjs} +113 -76
- package/dist/apply-Bu-8nYHL.mjs.map +1 -0
- package/dist/astro/index.d.mts +12 -11
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +13 -5
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +10 -9
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +52 -12
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +5 -5
- package/dist/astro/middleware/request-context.mjs +2 -2
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +2 -9
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +125 -71
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +6 -6
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +6 -6
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +4 -4
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +5 -5
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +4 -4
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +8 -8
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +8 -8
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +8 -8
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +14 -14
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +14 -14
- package/dist/astro/routes/api/admin/bylines/index.mjs +14 -14
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +12 -12
- package/dist/astro/routes/api/admin/comments/_id_.mjs +6 -6
- package/dist/astro/routes/api/admin/comments/bulk.mjs +10 -10
- package/dist/astro/routes/api/admin/comments/counts.mjs +6 -6
- package/dist/astro/routes/api/admin/comments/index.mjs +10 -10
- 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 +4 -4
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +4 -4
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +38 -37
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +38 -37
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +37 -36
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +36 -35
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +36 -35
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/index.mjs +37 -36
- package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +36 -35
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +36 -35
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +36 -35
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +36 -35
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +37 -36
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +36 -35
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +37 -36
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.mjs +36 -35
- package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +36 -35
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +36 -35
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
- 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 +6 -6
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +3 -3
- package/dist/astro/routes/api/admin/users/index.mjs +6 -6
- package/dist/astro/routes/api/auth/dev-bypass.mjs +5 -5
- package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
- package/dist/astro/routes/api/auth/invite/complete.mjs +10 -10
- package/dist/astro/routes/api/auth/invite/index.mjs +7 -7
- package/dist/astro/routes/api/auth/invite/register-options.mjs +9 -9
- package/dist/astro/routes/api/auth/logout.mjs +3 -3
- package/dist/astro/routes/api/auth/magic-link/send.mjs +9 -9
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +3 -3
- package/dist/astro/routes/api/auth/me.mjs +7 -7
- package/dist/astro/routes/api/auth/mode.mjs +1 -1
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +6 -6
- package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/options.mjs +11 -11
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +9 -9
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +10 -10
- package/dist/astro/routes/api/auth/passkey/verify.mjs +10 -10
- package/dist/astro/routes/api/auth/signup/complete.mjs +10 -10
- package/dist/astro/routes/api/auth/signup/request.mjs +9 -9
- package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +13 -13
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +12 -12
- 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 +10 -10
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +7 -7
- 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 +7 -7
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +13 -13
- 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 +7 -7
- package/dist/astro/routes/api/content/_collection_/authors.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/index.mjs +7 -7
- package/dist/astro/routes/api/content/_collection_/trash.mjs +7 -7
- package/dist/astro/routes/api/dashboard.mjs +8 -8
- package/dist/astro/routes/api/dev/emails.mjs +3 -3
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +19 -11
- package/dist/astro/routes/api/import/probe.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -40
- package/dist/astro/routes/api/import/wordpress/execute.d.mts.map +1 -1
- package/dist/astro/routes/api/import/wordpress/execute.mjs +12 -407
- package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -9
- package/dist/astro/routes/api/import/wordpress/media.d.mts.map +1 -1
- package/dist/astro/routes/api/import/wordpress/media.mjs +9 -9
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +19 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts.map +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +97 -79
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +19 -11
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +40 -2
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts.map +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +519 -39
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
- package/dist/astro/routes/api/manifest.d.mts.map +1 -1
- package/dist/astro/routes/api/manifest.mjs +14 -5
- package/dist/astro/routes/api/manifest.mjs.map +1 -1
- package/dist/astro/routes/api/mcp.mjs +39 -39
- package/dist/astro/routes/api/media/_id_/confirm.mjs +7 -7
- package/dist/astro/routes/api/media/_id_.mjs +7 -7
- 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 +8 -8
- package/dist/astro/routes/api/media.mjs +9 -9
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +9 -9
- package/dist/astro/routes/api/menus/_name_/items.mjs +9 -9
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +9 -9
- package/dist/astro/routes/api/menus/_name_/translations.mjs +9 -9
- package/dist/astro/routes/api/menus/_name_.mjs +9 -9
- package/dist/astro/routes/api/menus/index.mjs +9 -9
- package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/code.mjs +9 -9
- package/dist/astro/routes/api/oauth/device/token.mjs +8 -8
- package/dist/astro/routes/api/oauth/register.mjs +3 -3
- package/dist/astro/routes/api/oauth/token/refresh.mjs +6 -6
- package/dist/astro/routes/api/oauth/token/revoke.mjs +6 -6
- package/dist/astro/routes/api/oauth/token.mjs +6 -6
- package/dist/astro/routes/api/openapi.json.mjs +4 -4
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +4 -4
- package/dist/astro/routes/api/redirects/404s/index.mjs +9 -9
- package/dist/astro/routes/api/redirects/404s/summary.mjs +9 -9
- package/dist/astro/routes/api/redirects/_id_.mjs +10 -10
- package/dist/astro/routes/api/redirects/index.mjs +10 -10
- 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 +36 -35
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +36 -35
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +36 -35
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +36 -35
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/index.mjs +36 -35
- package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/index.mjs +7 -6
- package/dist/astro/routes/api/schema/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +36 -35
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/index.mjs +36 -35
- package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/enable.mjs +10 -10
- package/dist/astro/routes/api/search/index.mjs +9 -9
- package/dist/astro/routes/api/search/rebuild.mjs +10 -10
- package/dist/astro/routes/api/search/stats.mjs +6 -6
- package/dist/astro/routes/api/search/suggest.mjs +9 -9
- package/dist/astro/routes/api/sections/_slug_.mjs +9 -9
- package/dist/astro/routes/api/sections/index.mjs +9 -9
- package/dist/astro/routes/api/settings/email.mjs +4 -4
- package/dist/astro/routes/api/settings.mjs +12 -12
- package/dist/astro/routes/api/setup/admin-verify.mjs +11 -11
- package/dist/astro/routes/api/setup/admin.mjs +10 -10
- package/dist/astro/routes/api/setup/dev-bypass.d.mts.map +1 -1
- package/dist/astro/routes/api/setup/dev-bypass.mjs +28 -25
- package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
- package/dist/astro/routes/api/setup/index.mjs +26 -25
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/setup/status.mjs +4 -4
- package/dist/astro/routes/api/snapshot.mjs +6 -6
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +13 -13
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +13 -13
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +13 -13
- package/dist/astro/routes/api/taxonomies/index.mjs +13 -13
- package/dist/astro/routes/api/themes/preview.mjs +5 -5
- package/dist/astro/routes/api/typegen.mjs +5 -5
- package/dist/astro/routes/api/well-known/auth.mjs +1 -1
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +7 -7
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +9 -9
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +9 -9
- package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
- package/dist/astro/routes/api/widget-areas/index.mjs +9 -9
- package/dist/astro/routes/api/widget-components.mjs +3 -3
- package/dist/astro/routes/robots.txt.mjs +6 -6
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +9 -9
- package/dist/astro/routes/sitemap.xml.mjs +7 -7
- package/dist/astro/types.d.mts +14 -12
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/auth/providers/github.d.mts +1 -1
- package/dist/auth/providers/google.d.mts +1 -1
- package/dist/{authorize-BZnOGJFo.mjs → authorize-Dz88F_Fw.mjs} +2 -2
- package/dist/{authorize-BZnOGJFo.mjs.map → authorize-Dz88F_Fw.mjs.map} +1 -1
- package/dist/{byline-LL8lyqdb.mjs → byline-Bm8ZgUP9.mjs} +6 -6
- package/dist/{byline-LL8lyqdb.mjs.map → byline-Bm8ZgUP9.mjs.map} +1 -1
- package/dist/{byline-fields-eyJMblSr.mjs → byline-fields-2Lp5Hbgf.mjs} +18 -3
- package/dist/byline-fields-2Lp5Hbgf.mjs.map +1 -0
- package/dist/{byline-fields-DaMKzkhO.mjs → byline-fields-CIx_GuN4.mjs} +1 -1
- package/dist/{byline-fields-DaMKzkhO.mjs.map → byline-fields-CIx_GuN4.mjs.map} +1 -1
- package/dist/{byline-fields-KD3JFRuH.d.mts → byline-fields-Cgrt9-aL.d.mts} +18 -3
- package/dist/{byline-fields-KD3JFRuH.d.mts.map → byline-fields-Cgrt9-aL.d.mts.map} +1 -1
- package/dist/{bylines-BmJ7Vg7J.mjs → bylines-6tm9mgQb.mjs} +4 -4
- package/dist/{bylines-BmJ7Vg7J.mjs.map → bylines-6tm9mgQb.mjs.map} +1 -1
- package/dist/{bylines-DE47vP8d.mjs → bylines-Woq87129.mjs} +6 -6
- package/dist/{bylines-DE47vP8d.mjs.map → bylines-Woq87129.mjs.map} +1 -1
- package/dist/{cache-D9jF5bYw.mjs → cache-CEBAZjir.mjs} +2 -2
- package/dist/{cache-D9jF5bYw.mjs.map → cache-CEBAZjir.mjs.map} +1 -1
- package/dist/{challenge-store-LhiqMccz.mjs → challenge-store-anhwVER0.mjs} +1 -1
- package/dist/{challenge-store-LhiqMccz.mjs.map → challenge-store-anhwVER0.mjs.map} +1 -1
- package/dist/{chunks-OCG163rN.mjs → chunks-B2isWQhr.mjs} +2 -2
- package/dist/{chunks-OCG163rN.mjs.map → chunks-B2isWQhr.mjs.map} +1 -1
- package/dist/cli/index.mjs +23 -22
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +2 -2
- package/dist/{comment-D9BLDpZI.mjs → comment-CzeatwBV.mjs} +3 -3
- package/dist/{comment-D9BLDpZI.mjs.map → comment-CzeatwBV.mjs.map} +1 -1
- package/dist/{comment-reaction-Drl9SkNu.mjs → comment-reaction-Cj9QJY7I.mjs} +2 -2
- package/dist/{comment-reaction-Drl9SkNu.mjs.map → comment-reaction-Cj9QJY7I.mjs.map} +1 -1
- package/dist/{comments-BjJ9SKGU.mjs → comments-BLE-jv5k.mjs} +3 -3
- package/dist/{comments-BjJ9SKGU.mjs.map → comments-BLE-jv5k.mjs.map} +1 -1
- package/dist/{components-CYt4uVK9.mjs → components-B_sATNUc.mjs} +1 -1
- package/dist/{components-CYt4uVK9.mjs.map → components-B_sATNUc.mjs.map} +1 -1
- package/dist/{content-ncUWWjna.mjs → content-DhgEth4k.mjs} +4 -4
- package/dist/{content-ncUWWjna.mjs.map → content-DhgEth4k.mjs.map} +1 -1
- package/dist/content-refresh-CA4uKwzH.mjs +1531 -0
- package/dist/content-refresh-CA4uKwzH.mjs.map +1 -0
- package/dist/{context-C_g-iJVA.mjs → context-CUsR9dXL.mjs} +76 -54
- package/dist/context-CUsR9dXL.mjs.map +1 -0
- package/dist/{cron-C5LVoNmP.mjs → cron-v4FTEFR_.mjs} +1 -1
- package/dist/{cron-C5LVoNmP.mjs.map → cron-v4FTEFR_.mjs.map} +1 -1
- package/dist/{dashboard-CotyqQrW.mjs → dashboard-EvEKF5en.mjs} +4 -4
- package/dist/{dashboard-CotyqQrW.mjs.map → dashboard-EvEKF5en.mjs.map} +1 -1
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{default-CeRG-Ot4.mjs → default-CM_D83Mg.mjs} +1 -1
- package/dist/{default-CeRG-Ot4.mjs.map → default-CM_D83Mg.mjs.map} +1 -1
- package/dist/{device-flow-DENDCQ9F.mjs → device-flow-CfGP18Uw.mjs} +4 -4
- package/dist/{device-flow-DENDCQ9F.mjs.map → device-flow-CfGP18Uw.mjs.map} +1 -1
- package/dist/{email-console-DJP32ucW.mjs → email-console-CIf4Lfvv.mjs} +1 -1
- package/dist/{email-console-DJP32ucW.mjs.map → email-console-CIf4Lfvv.mjs.map} +1 -1
- package/dist/{error-rVGiqWDt.mjs → error-CkAseSqZ.mjs} +2 -2
- package/dist/{error-rVGiqWDt.mjs.map → error-CkAseSqZ.mjs.map} +1 -1
- package/dist/{escape-Bjio4ZsM.mjs → escape-DqD0aCKu.mjs} +1 -1
- package/dist/{escape-Bjio4ZsM.mjs.map → escape-DqD0aCKu.mjs.map} +1 -1
- package/dist/{field-defs-cache-ljXzJCfc.mjs → field-defs-cache-WUI88LX5.mjs} +2 -2
- package/dist/{field-defs-cache-ljXzJCfc.mjs.map → field-defs-cache-WUI88LX5.mjs.map} +1 -1
- package/dist/{fts-manager-BbCCbEKA.mjs → fts-manager-BbFaF_V8.mjs} +2 -2
- package/dist/{fts-manager-BbCCbEKA.mjs.map → fts-manager-BbFaF_V8.mjs.map} +1 -1
- package/dist/{import-Bb1T9WJS.mjs → import-fYLf14Co.mjs} +514 -25
- package/dist/import-fYLf14Co.mjs.map +1 -0
- package/dist/{index-BRNabKrx.d.mts → index-B9sIxwe4.d.mts} +2420 -2379
- package/dist/index-B9sIxwe4.d.mts.map +1 -0
- package/dist/{index-Dt0OKJB4.d.mts → index-Bm-Gy8Mh.d.mts} +25 -4
- package/dist/index-Bm-Gy8Mh.d.mts.map +1 -0
- package/dist/index.d.mts +18 -17
- package/dist/index.mjs +54 -53
- package/dist/{load-DMjXghbZ.mjs → load-Djn3HWQS.mjs} +2 -2
- package/dist/{load-DMjXghbZ.mjs.map → load-Djn3HWQS.mjs.map} +1 -1
- package/dist/{loader-BA_zhkS_.mjs → loader-CB6YHeul.mjs} +6 -5
- package/dist/loader-CB6YHeul.mjs.map +1 -0
- package/dist/{manifest-schema-kPGX7VS-.mjs → manifest-schema-BHlgZM1W.mjs} +1 -1
- package/dist/{manifest-schema-kPGX7VS-.mjs.map → manifest-schema-BHlgZM1W.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/local-runtime.d.mts +12 -11
- package/dist/media/local-runtime.d.mts.map +1 -1
- package/dist/media/local-runtime.mjs +5 -5
- package/dist/{media-C5t_eCnk.mjs → media-BrXIaMFN.mjs} +2 -2
- package/dist/{media-C5t_eCnk.mjs.map → media-BrXIaMFN.mjs.map} +1 -1
- package/dist/{media-allowlist-Du8t1u6a.mjs → media-allowlist-CRHEeJ_i.mjs} +1 -1
- package/dist/{media-allowlist-Du8t1u6a.mjs.map → media-allowlist-CRHEeJ_i.mjs.map} +1 -1
- package/dist/{media-url-O4rm9-SQ.mjs → media-url-CKi5XSw4.mjs} +1 -1
- package/dist/{media-url-O4rm9-SQ.mjs.map → media-url-CKi5XSw4.mjs.map} +1 -1
- package/dist/{menus-CBb6UrMU.mjs → menus-Cfivc2Ad.mjs} +3 -3
- package/dist/{menus-CBb6UrMU.mjs.map → menus-Cfivc2Ad.mjs.map} +1 -1
- package/dist/{menus-CYH09a41.mjs → menus-CsZ2ZEK2.mjs} +16 -12
- package/dist/{menus-CYH09a41.mjs.map → menus-CsZ2ZEK2.mjs.map} +1 -1
- package/dist/{mode-BB0F8xTC.mjs → mode-BM9QgEmt.mjs} +1 -1
- package/dist/{mode-BB0F8xTC.mjs.map → mode-BM9QgEmt.mjs.map} +1 -1
- package/dist/{oauth-authorization-CsvzIp_F.mjs → oauth-authorization-CXWkIGT4.mjs} +4 -4
- package/dist/{oauth-authorization-CsvzIp_F.mjs.map → oauth-authorization-CXWkIGT4.mjs.map} +1 -1
- package/dist/{oauth-clients-C9SYwEbZ.mjs → oauth-clients-BmTI5rsl.mjs} +1 -1
- package/dist/{oauth-clients-C9SYwEbZ.mjs.map → oauth-clients-BmTI5rsl.mjs.map} +1 -1
- package/dist/{oauth-state-store---zrApfB.mjs → oauth-state-store-YLFLcH5F.mjs} +1 -1
- package/dist/{oauth-state-store---zrApfB.mjs.map → oauth-state-store-YLFLcH5F.mjs.map} +1 -1
- package/dist/{oauth-user-lookup-SHsWRlG9.mjs → oauth-user-lookup-Bsc_GsX-.mjs} +1 -1
- package/dist/{oauth-user-lookup-SHsWRlG9.mjs.map → oauth-user-lookup-Bsc_GsX-.mjs.map} +1 -1
- package/dist/object-cache/memory.d.mts +1 -1
- package/dist/{object-cache-CHbHv83-.mjs → object-cache-Bok5j2ae.mjs} +43 -3
- package/dist/object-cache-Bok5j2ae.mjs.map +1 -0
- package/dist/{options-41nCWqi9.d.mts → options-iMpRRxIe.d.mts} +3 -3
- package/dist/{options-41nCWqi9.d.mts.map → options-iMpRRxIe.d.mts.map} +1 -1
- package/dist/page/index.d.mts +2 -2
- package/dist/{parse-BwAdS7FZ.mjs → parse-GmjecGct.mjs} +2 -2
- package/dist/{parse-BwAdS7FZ.mjs.map → parse-GmjecGct.mjs.map} +1 -1
- package/dist/{passkey-config-C0YfSBko.mjs → passkey-config-De0ahnRg.mjs} +1 -1
- package/dist/{passkey-config-C0YfSBko.mjs.map → passkey-config-De0ahnRg.mjs.map} +1 -1
- package/dist/{placeholder-Cuce9U-m.d.mts → placeholder-BSKwzRe4.d.mts} +1 -1
- package/dist/{placeholder-Cuce9U-m.d.mts.map → placeholder-BSKwzRe4.d.mts.map} +1 -1
- package/dist/plugin-types.d.mts +1 -1
- package/dist/plugin-utils.d.mts +10 -9
- package/dist/plugin-utils.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +10 -9
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
- package/dist/{portable-text-BICg8bTk.mjs → portable-text-cCa0cGV6.mjs} +1 -1
- package/dist/{portable-text-BICg8bTk.mjs.map → portable-text-cCa0cGV6.mjs.map} +1 -1
- package/dist/{preview-DKGCt2_p.mjs → preview-SSXNVPAC.mjs} +2 -2
- package/dist/{preview-DKGCt2_p.mjs.map → preview-SSXNVPAC.mjs.map} +1 -1
- package/dist/{public-url-CTVqgMmg.mjs → public-url-Danv47cS.mjs} +1 -1
- package/dist/{public-url-CTVqgMmg.mjs.map → public-url-Danv47cS.mjs.map} +1 -1
- package/dist/{query-BpQb-blG.mjs → query-ByGq6ver.mjs} +14 -14
- package/dist/{query-BpQb-blG.mjs.map → query-ByGq6ver.mjs.map} +1 -1
- package/dist/{rate-limit-Dap6HMNM.mjs → rate-limit-C_Lf63Dd.mjs} +2 -2
- package/dist/{rate-limit-Dap6HMNM.mjs.map → rate-limit-C_Lf63Dd.mjs.map} +1 -1
- package/dist/{redirect-CS-PHtNh.mjs → redirect-CoW3G-0U.mjs} +1 -1
- package/dist/{redirect-CS-PHtNh.mjs.map → redirect-CoW3G-0U.mjs.map} +1 -1
- package/dist/{redirect-_M0trWoo.mjs → redirect-IyfNirgS.mjs} +2 -2
- package/dist/{redirect-_M0trWoo.mjs.map → redirect-IyfNirgS.mjs.map} +1 -1
- package/dist/{redirects-w0V9vj_j.mjs → redirects-Bs1g42Ri.mjs} +4 -4
- package/dist/{redirects-w0V9vj_j.mjs.map → redirects-Bs1g42Ri.mjs.map} +1 -1
- package/dist/{redirects-Dcbuisoj.mjs → redirects-DXEXBAII.mjs} +2 -2
- package/dist/{redirects-Dcbuisoj.mjs.map → redirects-DXEXBAII.mjs.map} +1 -1
- package/dist/{registry-g8yYt4cC.mjs → registry-B65mUtPa.mjs} +122 -79
- package/dist/registry-B65mUtPa.mjs.map +1 -0
- package/dist/{request-meta-CmS1tDFf.mjs → request-meta-DbvXaAvU.mjs} +2 -2
- package/dist/{request-meta-CmS1tDFf.mjs.map → request-meta-DbvXaAvU.mjs.map} +1 -1
- package/dist/{resolve-C7I0qiR0.mjs → resolve-DR3wqMTy.mjs} +1 -1
- package/dist/{resolve-C7I0qiR0.mjs.map → resolve-DR3wqMTy.mjs.map} +1 -1
- package/dist/{runner-BbR3DfrL.d.mts → runner-DvFWoz8-.d.mts} +2 -2
- package/dist/{runner-BbR3DfrL.d.mts.map → runner-DvFWoz8-.d.mts.map} +1 -1
- package/dist/{runner-DPdt_Wpa.mjs → runner-K8XK101B.mjs} +351 -199
- package/dist/runner-K8XK101B.mjs.map +1 -0
- package/dist/runtime.d.mts +11 -10
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +2 -2
- package/dist/scheduled-publish-Drffrf2a.d.mts +11 -0
- package/dist/scheduled-publish-Drffrf2a.d.mts.map +1 -0
- package/dist/{schema-DoLd3j2e.mjs → schema-Blhoxp3P.mjs} +5 -5
- package/dist/{schema-DoLd3j2e.mjs.map → schema-Blhoxp3P.mjs.map} +1 -1
- package/dist/{search-BR0ZeZmh.mjs → search-DZN2HZ-V.mjs} +6 -4
- package/dist/search-DZN2HZ-V.mjs.map +1 -0
- package/dist/{secrets-BSf9pRRY.mjs → secrets-kp760LlD.mjs} +2 -2
- package/dist/{secrets-BSf9pRRY.mjs.map → secrets-kp760LlD.mjs.map} +1 -1
- package/dist/{sections-5J2YJPqJ.mjs → sections-CpF5SaGr.mjs} +3 -3
- package/dist/{sections-5J2YJPqJ.mjs.map → sections-CpF5SaGr.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +19 -18
- package/dist/seo/index.d.mts +1 -1
- package/dist/seo/index.mjs +1 -1
- package/dist/{seo-BkhuuaaE.mjs → seo-89-53DDs.mjs} +1 -1
- package/dist/{seo-BkhuuaaE.mjs.map → seo-89-53DDs.mjs.map} +1 -1
- package/dist/{seo-DKeXHkzE.mjs → seo-DXu2UQnC.mjs} +3 -3
- package/dist/{seo-DKeXHkzE.mjs.map → seo-DXu2UQnC.mjs.map} +1 -1
- package/dist/{service-BbO2s8gT.mjs → service-DQmiBMzt.mjs} +3 -3
- package/dist/{service-BbO2s8gT.mjs.map → service-DQmiBMzt.mjs.map} +1 -1
- package/dist/{session-user-B8aLtKAH.mjs → session-user-Cascbax7.mjs} +1 -1
- package/dist/{session-user-B8aLtKAH.mjs.map → session-user-Cascbax7.mjs.map} +1 -1
- package/dist/{settings-BMNr9Dmi.mjs → settings-rZyHEcNT.mjs} +3 -3
- package/dist/{settings-BMNr9Dmi.mjs.map → settings-rZyHEcNT.mjs.map} +1 -1
- package/dist/{settings-DJeki-97.mjs → settings-zhmdKyJQ.mjs} +5 -5
- package/dist/{settings-DJeki-97.mjs.map → settings-zhmdKyJQ.mjs.map} +1 -1
- package/dist/{setup-complete-gEiySUc-.mjs → setup-complete-Cb3LRyzy.mjs} +1 -1
- package/dist/{setup-complete-gEiySUc-.mjs.map → setup-complete-Cb3LRyzy.mjs.map} +1 -1
- package/dist/{setup-nonce-DzS50zme.mjs → setup-nonce-Z8x0tHf8.mjs} +1 -1
- package/dist/{setup-nonce-DzS50zme.mjs.map → setup-nonce-Z8x0tHf8.mjs.map} +1 -1
- package/dist/{single-flight-cache-Cdfkic3t.mjs → single-flight-cache-BG3B_GhN.mjs} +1 -1
- package/dist/{single-flight-cache-Cdfkic3t.mjs.map → single-flight-cache-BG3B_GhN.mjs.map} +1 -1
- package/dist/{site-url-BLebyON8.mjs → site-url-BBfDTgZB.mjs} +1 -1
- package/dist/{site-url-BLebyON8.mjs.map → site-url-BBfDTgZB.mjs.map} +1 -1
- package/dist/{ssrf-BRKb343l.mjs → ssrf-1g7usGBg.mjs} +1 -1
- package/dist/{ssrf-BRKb343l.mjs.map → ssrf-1g7usGBg.mjs.map} +1 -1
- package/dist/{status-vUK0SA17.mjs → status-CqJQX1QN.mjs} +1 -1
- package/dist/{status-vUK0SA17.mjs.map → status-CqJQX1QN.mjs.map} +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/{taxonomies-CM_fUqLU.mjs → taxonomies-BGBd16Eu.mjs} +4 -4
- package/dist/{taxonomies-CM_fUqLU.mjs.map → taxonomies-BGBd16Eu.mjs.map} +1 -1
- package/dist/{taxonomies-D13i2ctt.mjs → taxonomies-Cnb4F-Pb.mjs} +7 -7
- package/dist/{taxonomies-D13i2ctt.mjs.map → taxonomies-Cnb4F-Pb.mjs.map} +1 -1
- package/dist/{taxonomy-BU0LjdVb.mjs → taxonomy-BxGxZZdu.mjs} +4 -4
- package/dist/{taxonomy-BU0LjdVb.mjs.map → taxonomy-BxGxZZdu.mjs.map} +1 -1
- package/dist/{tokens-LeuXF9gG.mjs → tokens-BGT0TUUo.mjs} +1 -1
- package/dist/{tokens-LeuXF9gG.mjs.map → tokens-BGT0TUUo.mjs.map} +1 -1
- package/dist/{transport-BZ-03aFT.d.mts → transport-BMDWHSgZ.d.mts} +1 -1
- package/dist/{transport-BZ-03aFT.d.mts.map → transport-BMDWHSgZ.d.mts.map} +1 -1
- package/dist/{transport-_2nBz7e9.mjs → transport-DPefPMcz.mjs} +1 -1
- package/dist/{transport-_2nBz7e9.mjs.map → transport-DPefPMcz.mjs.map} +1 -1
- package/dist/{trusted-proxy-DZY5WCn2.mjs → trusted-proxy-VTPRiX2s.mjs} +1 -1
- package/dist/{trusted-proxy-DZY5WCn2.mjs.map → trusted-proxy-VTPRiX2s.mjs.map} +1 -1
- package/dist/{types-ETmO_jQr.d.mts → types-BcLUUbbR.d.mts} +2 -2
- package/dist/{types-ETmO_jQr.d.mts.map → types-BcLUUbbR.d.mts.map} +1 -1
- package/dist/{types-BYnDBVwe.d.mts → types-CGbYowGQ.d.mts} +2 -2
- package/dist/{types-BYnDBVwe.d.mts.map → types-CGbYowGQ.d.mts.map} +1 -1
- package/dist/{types-BWqGVhFn.d.mts → types-CgvhU-hF.d.mts} +1 -1
- package/dist/{types-BWqGVhFn.d.mts.map → types-CgvhU-hF.d.mts.map} +1 -1
- package/dist/{types-fS7-VTdR.d.mts → types-ClUkdbWb.d.mts} +1 -1
- package/dist/{types-fS7-VTdR.d.mts.map → types-ClUkdbWb.d.mts.map} +1 -1
- package/dist/{types-CZgK3EMW.mjs → types-CuW0zWDA.mjs} +2 -2
- package/dist/{types-CZgK3EMW.mjs.map → types-CuW0zWDA.mjs.map} +1 -1
- package/dist/{types-BG8_XTLj.d.mts → types-DmHk7vAt.d.mts} +1 -1
- package/dist/{types-BG8_XTLj.d.mts.map → types-DmHk7vAt.d.mts.map} +1 -1
- package/dist/{types-CQAugunJ.mjs → types-Jo3UYd6b.mjs} +1 -1
- package/dist/{types-CQAugunJ.mjs.map → types-Jo3UYd6b.mjs.map} +1 -1
- package/dist/{types-xIfVRNLp.d.mts → types-_HFLRzTn.d.mts} +22 -1
- package/dist/types-_HFLRzTn.d.mts.map +1 -0
- package/dist/{types-DrYMm9s5.d.mts → types-ea9sAPck.d.mts} +1 -1
- package/dist/{types-DrYMm9s5.d.mts.map → types-ea9sAPck.d.mts.map} +1 -1
- package/dist/{types-CNlaBFzx.d.mts → types-g_aFIdW0.d.mts} +1 -1
- package/dist/{types-CNlaBFzx.d.mts.map → types-g_aFIdW0.d.mts.map} +1 -1
- package/dist/{types-CUsXGRVj.d.mts → types-vgEWF18A.d.mts} +19 -1
- package/dist/{types-CUsXGRVj.d.mts.map → types-vgEWF18A.d.mts.map} +1 -1
- package/dist/{user-DkG2jKCp.mjs → user-BjAOvU3Z.mjs} +3 -3
- package/dist/{user-DkG2jKCp.mjs.map → user-BjAOvU3Z.mjs.map} +1 -1
- package/dist/{utils-B7A57fm9.mjs → utils-BJXElxdw.mjs} +156 -7
- package/dist/utils-BJXElxdw.mjs.map +1 -0
- package/dist/{validate-CuJ1Ps9j.mjs → validate-CWSnKVIJ.mjs} +2 -2
- package/dist/{validate-CuJ1Ps9j.mjs.map → validate-CWSnKVIJ.mjs.map} +1 -1
- package/dist/{validate-CG0V0wNR.d.mts → validate-Z1b86_3B.d.mts} +11 -6
- package/dist/{validate-CG0V0wNR.d.mts.map → validate-Z1b86_3B.d.mts.map} +1 -1
- package/dist/{validation-CwUfTdGF.mjs → validation-c8WjhH8z.mjs} +8 -5
- package/dist/{validation-CwUfTdGF.mjs.map → validation-c8WjhH8z.mjs.map} +1 -1
- package/dist/version-CaccU7DM.mjs +7 -0
- package/dist/{version-BoNBItP_.mjs.map → version-CaccU7DM.mjs.map} +1 -1
- package/dist/{widgets-BfOZ-ajY.mjs → widgets-hWcIicX8.mjs} +3 -3
- package/dist/{widgets-BfOZ-ajY.mjs.map → widgets-hWcIicX8.mjs.map} +1 -1
- package/dist/wxr-taxonomies-0_uxSrmf.d.mts +34 -0
- package/dist/wxr-taxonomies-0_uxSrmf.d.mts.map +1 -0
- package/dist/wxr-taxonomies-BC3OYpvx.mjs +425 -0
- package/dist/wxr-taxonomies-BC3OYpvx.mjs.map +1 -0
- package/dist/{zod-generator-gFsRuNNy.mjs → zod-generator-CacrR-i8.mjs} +2 -2
- package/dist/{zod-generator-gFsRuNNy.mjs.map → zod-generator-CacrR-i8.mjs.map} +1 -1
- package/package.json +6 -6
- package/src/api/handlers/content.ts +69 -23
- package/src/api/handlers/plugins.ts +77 -10
- package/src/api/schemas/import.ts +19 -0
- package/src/astro/integration/runtime.ts +3 -6
- package/src/astro/integration/vite-config.ts +19 -1
- package/src/astro/middleware/auth.ts +10 -3
- package/src/astro/middleware/csp.ts +54 -3
- package/src/astro/middleware.ts +1 -0
- package/src/astro/routes/admin.astro +9 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +6 -1
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +6 -1
- package/src/astro/routes/api/admin/plugins/[id]/index.ts +1 -0
- package/src/astro/routes/api/admin/plugins/index.ts +1 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +122 -98
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +794 -44
- package/src/astro/routes/api/manifest.ts +20 -1
- package/src/astro/routes/api/setup/dev-bypass.ts +7 -2
- package/src/astro/types.ts +4 -0
- package/src/cli/commands/seed.ts +1 -1
- package/src/components/EmDashImage.astro +59 -23
- package/src/components/Image.astro +36 -35
- package/src/components/LiveSearch.astro +40 -4
- package/src/components/live-search-routing.ts +30 -0
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +23 -18
- package/src/database/migrations/048_restore_content_taxonomies_term_index.ts +32 -0
- package/src/database/migrations/049_taxonomies_name_locale_index.ts +42 -0
- package/src/database/migrations/050_media_usage_index_status.ts +142 -0
- package/src/database/migrations/runner.ts +6 -0
- package/src/database/repositories/media-usage.ts +932 -58
- package/src/database/types.ts +22 -0
- package/src/emdash-runtime.ts +123 -3
- package/src/import/comments.ts +159 -0
- package/src/import/index.ts +8 -1
- package/src/import/settings.ts +65 -189
- package/src/import/sources/wordpress-plugin.ts +338 -42
- package/src/import/sources/wxr.ts +4 -1
- package/src/import/types.ts +12 -0
- package/src/import/utils.ts +207 -0
- package/src/import/wxr-taxonomies.ts +39 -0
- package/src/loader.ts +13 -2
- package/src/media/usage/content-fields.ts +119 -0
- package/src/media/usage/content-refresh.ts +524 -0
- package/src/media/usage/content-repair.ts +491 -0
- package/src/media/usage/content-snapshots.ts +435 -0
- package/src/media/usage/extractor.ts +4 -4
- package/src/media/usage/source-key.ts +22 -0
- package/src/menus/index.ts +17 -1
- package/src/object-cache/index.ts +82 -7
- package/src/plugins/context.ts +100 -71
- package/src/schema/registry.ts +240 -154
- package/src/search/query.ts +3 -0
- package/src/search/types.ts +2 -0
- package/src/seed/apply.ts +158 -101
- package/src/seed/types.ts +6 -1
- package/dist/api-BMfveMoB.mjs.map +0 -1
- package/dist/apply--IVNSdXn.mjs.map +0 -1
- package/dist/byline-fields-eyJMblSr.mjs.map +0 -1
- package/dist/context-C_g-iJVA.mjs.map +0 -1
- package/dist/import-Bb1T9WJS.mjs.map +0 -1
- package/dist/index-BRNabKrx.d.mts.map +0 -1
- package/dist/index-Dt0OKJB4.d.mts.map +0 -1
- package/dist/loader-BA_zhkS_.mjs.map +0 -1
- package/dist/object-cache-CHbHv83-.mjs.map +0 -1
- package/dist/registry-g8yYt4cC.mjs.map +0 -1
- package/dist/runner-DPdt_Wpa.mjs.map +0 -1
- package/dist/search-BR0ZeZmh.mjs.map +0 -1
- package/dist/types-xIfVRNLp.d.mts.map +0 -1
- package/dist/utils-B7A57fm9.mjs.map +0 -1
- package/dist/version-BoNBItP_.mjs +0 -7
- /package/dist/{api-tokens-B4BQybOp.mjs → api-tokens-DsvvCtdh.mjs} +0 -0
- /package/dist/{ssrf-CcX9zvMK.mjs → ssrf-CmuGgCq3.mjs} +0 -0
- /package/dist/{types-BvbeGEtr.mjs → types-DROWRxYV.mjs} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wxr-taxonomies-BC3OYpvx.mjs","names":[],"sources":["../src/import/wxr-taxonomies.ts"],"sourcesContent":["/**\n * WXR taxonomy import helpers.\n *\n * Bridges parsed WordPress taxonomy data (`WxrCategory`, `WxrTag`, `WxrTerm`,\n * and per-item `WxrPost.categories` / `WxrPost.tags` / `WxrPost.customTaxonomies`)\n * onto EmDash's term + content_taxonomies tables.\n *\n * Why this isn't inline in `execute.ts`: pre-creating all terms before any\n * post is created lets us (a) build a lookup once for every (taxonomy, slug)\n * the import needs, and (b) keep the per-post attachment loop cheap. It also\n * makes the logic testable without spinning up an Astro request.\n *\n * Behaviour:\n * - `wp:category` -> EmDash `category` taxonomy (seeded by migration 006).\n * - `wp:tag` -> EmDash `tag` taxonomy.\n * - `wp:term` -> matching EmDash taxonomy by `name` (case-sensitive).\n * If no matching def exists in the target locale, the\n * term is skipped — we don't auto-create defs because\n * the user controls their schema through the admin.\n * - Terms are created idempotently by `(taxonomy, slug, locale)`. Existing\n * terms are reused.\n * - Assignments respect the def's `collections` array. If the post's target\n * collection isn't listed on the taxonomy def, the assignment is skipped\n * (matches admin UI behaviour: you can't tag a \"products\" post with a\n * \"category\" if `category.collections` only includes \"posts\").\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { WxrCategory, WxrPost, WxrTag, WxrTerm } from \"../cli/wxr/parser.js\";\nimport { TaxonomyRepository } from \"../database/repositories/taxonomy.js\";\nimport type { Database } from \"../database/types.js\";\nimport { resolveLocaleChain } from \"../i18n/resolve.js\";\nimport { invalidateTermCache } from \"../taxonomies/index.js\";\n\n/**\n * Thrown by `mirrorTermsToLocales` when a pre-existing locale row at the\n * same `(taxonomy, slug)` belongs to a different `translation_group` than\n * the canonical term. Callers in the route layer surface\n * `publicMessage` to the operator (no internal data) while logging\n * `cause` server-side.\n *\n * Marker class so the route layer can distinguish \"operator-actionable\n * taxonomy conflict\" from any other DB / repository error that might\n * escape the helper.\n */\nexport class WxrTaxonomyConflictError extends Error {\n\treadonly publicMessage: string;\n\tconstructor(publicMessage: string, options?: { cause?: unknown }) {\n\t\tsuper(publicMessage, options);\n\t\tthis.name = \"WxrTaxonomyConflictError\";\n\t\tthis.publicMessage = publicMessage;\n\t}\n}\n\nexport function isWxrTaxonomyConflictError(error: unknown): error is WxrTaxonomyConflictError {\n\treturn error instanceof WxrTaxonomyConflictError;\n}\n\n/**\n * Result of pre-importing taxonomy terms from a WXR file.\n */\nexport interface TaxonomyImportPlan {\n\t/** terms created during this run (per taxonomy name) */\n\ttermsCreated: Record<string, number>;\n\t/** terms that already existed and were reused (per taxonomy name) */\n\ttermsReused: Record<string, number>;\n\t/** custom taxonomies (`wp:term`) skipped because no matching EmDash def exists */\n\tmissingTaxonomies: string[];\n\t/**\n\t * Lookup table: `taxonomy name` -> `term slug` -> term id.\n\t * Used by `attachPostTaxonomies` to translate WXR assignments into pivot rows.\n\t */\n\ttermIdByNameAndSlug: Map<string, Map<string, string>>;\n\t/**\n\t * Lookup table: `taxonomy name` -> set of collection slugs the def allows.\n\t * Empty (or missing) means \"any collection\" — we only enforce the filter\n\t * when the def explicitly lists collections.\n\t */\n\tcollectionsByTaxonomy: Map<string, Set<string>>;\n\t/**\n\t * Lookup table: `term id` -> the term's `translation_group` (or `null`\n\t * if the term doesn't exist any more). Populated lazily by helpers that\n\t * need to check pivot existence without repeating per-term DB reads.\n\t */\n\ttranslationGroupByTermId: Map<string, string | null>;\n}\n\n/**\n * Track running counts plus the lookup maps.\n */\ninterface TaxonomyImportState {\n\tplan: TaxonomyImportPlan;\n}\n\nfunction makeState(): TaxonomyImportState {\n\treturn {\n\t\tplan: {\n\t\t\ttermsCreated: {},\n\t\t\ttermsReused: {},\n\t\t\tmissingTaxonomies: [],\n\t\t\ttermIdByNameAndSlug: new Map(),\n\t\t\tcollectionsByTaxonomy: new Map(),\n\t\t\ttranslationGroupByTermId: new Map(),\n\t\t},\n\t};\n}\n\n/**\n * Record-keeping helpers — keep mutations centralised so the result object\n * stays consistent.\n */\nfunction bump(record: Record<string, number>, key: string): void {\n\trecord[key] = (record[key] ?? 0) + 1;\n}\n\nfunction rememberTerm(\n\tstate: TaxonomyImportState,\n\ttaxonomyName: string,\n\tslug: string,\n\ttermId: string,\n): void {\n\tlet bySlug = state.plan.termIdByNameAndSlug.get(taxonomyName);\n\tif (!bySlug) {\n\t\tbySlug = new Map();\n\t\tstate.plan.termIdByNameAndSlug.set(taxonomyName, bySlug);\n\t}\n\tbySlug.set(slug, termId);\n}\n\n/**\n * Look up an EmDash taxonomy def by name. Definitions are per-locale but\n * a def is conceptually site-wide -- the per-locale row carries only the\n * label translations.\n *\n * Match the runtime helper `getTaxonomyDef` (in `src/taxonomies/index.ts`):\n * walk `resolveLocaleChain(locale)` so the importer picks the same def the\n * runtime would later resolve to. When the chain is empty (i18n disabled)\n * or every locale in the chain misses, fall through to the lowest-locale\n * row so single-locale installs still see seeded defs that were inserted\n * at some non-empty locale value.\n *\n * Without this fallback, a user importing into a non-default locale would\n * see every category dropped as `missingTaxonomies` even though the seeded\n * defs exist (just at the site's default locale).\n */\nfunction parseDefCollections(raw: string | null): string[] {\n\tif (!raw) return [];\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(raw);\n\t\tif (Array.isArray(parsed)) {\n\t\t\treturn parsed.filter((c): c is string => typeof c === \"string\");\n\t\t}\n\t} catch {\n\t\t// malformed JSON in the def -- treat as \"no collection filter\"\n\t}\n\treturn [];\n}\n\nasync function findTaxonomyDef(\n\tdb: Kysely<Database>,\n\tname: string,\n\tlocale: string | undefined,\n): Promise<{ id: string; collections: string[] } | null> {\n\tconst chain = resolveLocaleChain(locale);\n\n\tif (chain.length === 0) {\n\t\t// i18n disabled and no explicit locale. The runtime treats this\n\t\t// as \"no locale filter\" and picks the lowest-locale row. We do the\n\t\t// same so the importer agrees with how the runtime later reads\n\t\t// the def.\n\t\tconst row = await db\n\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t.selectAll()\n\t\t\t.where(\"name\", \"=\", name)\n\t\t\t.orderBy(\"locale\", \"asc\")\n\t\t\t.executeTakeFirst();\n\t\treturn row ? { id: row.id, collections: parseDefCollections(row.collections) } : null;\n\t}\n\n\t// Non-empty chain: walk it in order, return null if every entry misses.\n\t// This matches `getTaxonomyDef` exactly. We deliberately do NOT fall\n\t// through to any-locale lookup: doing so would let the importer pick a\n\t// def at a locale the runtime would never resolve to, producing\n\t// content the user can't see in the admin or on the rendered site.\n\tfor (const tryLocale of chain) {\n\t\tconst row = await db\n\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t.selectAll()\n\t\t\t.where(\"name\", \"=\", name)\n\t\t\t.where(\"locale\", \"=\", tryLocale)\n\t\t\t.executeTakeFirst();\n\t\tif (row) {\n\t\t\treturn { id: row.id, collections: parseDefCollections(row.collections) };\n\t\t}\n\t}\n\n\treturn null;\n}\n\n/**\n * Rebuild the taxonomy lookup maps from the database without creating\n * anything. The chunked WP import runs term creation once (first content\n * chunk); every later chunk only needs the maps to attach per-post\n * assignments, and two SELECTs are far cheaper than re-walking every term\n * through the ensure path.\n *\n * ponytail: first row wins per (name, slug), ordered by locale asc — the\n * same row `findBySlug` without a locale resolves to. Per-locale term\n * variants beyond that are the mirror pass's job, not the importer's.\n */\nexport async function loadTaxonomyPlanFromDb(db: Kysely<Database>): Promise<TaxonomyImportPlan> {\n\tconst state = makeState();\n\n\tconst defs = await db\n\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t.select([\"name\", \"collections\"])\n\t\t.orderBy(\"locale\", \"asc\")\n\t\t.execute();\n\tfor (const def of defs) {\n\t\tif (!state.plan.collectionsByTaxonomy.has(def.name)) {\n\t\t\tstate.plan.collectionsByTaxonomy.set(def.name, new Set(parseDefCollections(def.collections)));\n\t\t}\n\t}\n\n\tconst terms = await db\n\t\t.selectFrom(\"taxonomies\")\n\t\t.select([\"name\", \"slug\", \"id\"])\n\t\t.orderBy(\"locale\", \"asc\")\n\t\t.execute();\n\tfor (const term of terms) {\n\t\tif (!state.plan.termIdByNameAndSlug.get(term.name)?.has(term.slug)) {\n\t\t\trememberTerm(state, term.name, term.slug, term.id);\n\t\t}\n\t}\n\n\treturn state.plan;\n}\n\n/**\n * Find or create a term in the given taxonomy. Returns the term id. Callers\n * must verify the taxonomy def exists before calling — this helper assumes\n * the def is present.\n *\n * Note: we don't resolve WordPress parent slugs into EmDash parent ids in\n * this pass. WXR exports list categories in arbitrary order, so a category's\n * parent may not exist yet when we first see it. Hierarchy is preserved at\n * the data level (the parent slug is on `WxrCategory.parent`) but flattens\n * in EmDash for now; restoring the tree is a follow-up improvement.\n */\nasync function ensureTerm(\n\trepo: TaxonomyRepository,\n\tstate: TaxonomyImportState,\n\ttaxonomyName: string,\n\tslug: string,\n\tlabel: string,\n\tdescription: string | undefined,\n\tlocale: string | undefined,\n): Promise<string> {\n\t// Already resolved in this run (e.g. seen in `wp:category` AND in a per-\n\t// item `<category>` element).\n\tconst cached = state.plan.termIdByNameAndSlug.get(taxonomyName)?.get(slug);\n\tif (cached) return cached;\n\n\tconst existing = await repo.findBySlug(taxonomyName, slug, locale);\n\tif (existing) {\n\t\tbump(state.plan.termsReused, taxonomyName);\n\t\trememberTerm(state, taxonomyName, slug, existing.id);\n\t\treturn existing.id;\n\t}\n\n\t// No row at the requested locale. Before creating, check whether a\n\t// `(name, slug)` row exists in some OTHER locale -- e.g. the admin\n\t// pre-created an Arabic translation, and now an `en` import wants the\n\t// canonical row. We need to mint the new row inside the existing row's\n\t// `translation_group` so per-locale lookups across the family work.\n\t// Without this, the mirror pass would later refuse to reconcile (it\n\t// sees pre-existing rows in a different group as a no-op) and pivots\n\t// would point at a group that has no row in the requested locale.\n\tconst anyLocale = await repo.findBySlug(taxonomyName, slug);\n\tconst translationOf = anyLocale?.id;\n\n\tconst created = await repo.create({\n\t\tname: taxonomyName,\n\t\tslug,\n\t\tlabel,\n\t\tdata: description ? { description } : undefined,\n\t\tlocale,\n\t\ttranslationOf,\n\t});\n\tbump(state.plan.termsCreated, taxonomyName);\n\trememberTerm(state, taxonomyName, slug, created.id);\n\treturn created.id;\n}\n\n/**\n * Retrieve the human label captured by the parser for a per-item\n * `<category>` text body, falling back to the slug when the parser didn't\n * see a label (e.g. self-closing tags or whitespace-only bodies).\n */\nfunction labelFor(post: WxrPost, taxonomy: string, slug: string): string {\n\tconst key = `${taxonomy}\\u0000${slug}`;\n\treturn post.taxonomyLabels?.get(key) ?? slug;\n}\n\n/**\n * Pre-import every term referenced by the WXR file.\n *\n * Pass 1: `wp:category` blocks. Each becomes a term in EmDash's seeded\n * `category` taxonomy.\n * Pass 2: `wp:tag` blocks. Each becomes a term in `tag`.\n * Pass 3: `wp:term` blocks (custom taxonomies). Skipped when no matching\n * EmDash def exists.\n * Pass 4: per-item `<category domain=\"…\" nicename=\"…\">` assignments. WXR\n * exports sometimes reference taxonomies/terms that weren't declared\n * at the top level (older exports especially), so we backfill terms\n * from per-item assignments. Categories and tags use the seeded defs\n * and pick up the assignment text as the label; custom domains fall\n * back to the same \"def must exist\" rule.\n */\nexport async function preImportWxrTaxonomies(\n\tdb: Kysely<Database>,\n\tposts: WxrPost[],\n\tcategories: WxrCategory[],\n\ttags: WxrTag[],\n\tterms: WxrTerm[],\n\tlocale: string | undefined,\n): Promise<TaxonomyImportPlan> {\n\tconst state = makeState();\n\tconst repo = new TaxonomyRepository(db);\n\n\t// Cache def lookups for the duration of the import. Keyed by name; value\n\t// is `null` when we've already determined the def is missing in this\n\t// locale (so we only report the \"missing\" warning once per taxonomy).\n\tconst defCache = new Map<string, { id: string; collections: string[] } | null>();\n\tconst lookupDef = async (name: string): Promise<{ id: string; collections: string[] } | null> => {\n\t\tif (defCache.has(name)) return defCache.get(name) ?? null;\n\t\tconst def = await findTaxonomyDef(db, name, locale);\n\t\tdefCache.set(name, def);\n\t\tif (def) {\n\t\t\tstate.plan.collectionsByTaxonomy.set(name, new Set(def.collections));\n\t\t}\n\t\treturn def;\n\t};\n\n\t// Pass 1: top-level <wp:category> blocks -> EmDash `category` taxonomy.\n\tconst categoryDef = await lookupDef(\"category\");\n\tif (categoryDef) {\n\t\tfor (const cat of categories) {\n\t\t\tconst slug = cat.nicename;\n\t\t\tconst label = cat.name;\n\t\t\tif (!slug || !label) continue;\n\t\t\tawait ensureTerm(repo, state, \"category\", slug, label, cat.description, locale);\n\t\t}\n\t} else if (categories.length > 0) {\n\t\t// Seeded `category` def was deleted by the user — record so the\n\t\t// import response can surface why none of the categories landed.\n\t\tstate.plan.missingTaxonomies.push(\"category\");\n\t}\n\n\t// Pass 2: top-level <wp:tag> blocks -> EmDash `tag` taxonomy.\n\tconst tagDef = await lookupDef(\"tag\");\n\tif (tagDef) {\n\t\tfor (const tag of tags) {\n\t\t\tconst slug = tag.slug;\n\t\t\tconst label = tag.name;\n\t\t\tif (!slug || !label) continue;\n\t\t\tawait ensureTerm(repo, state, \"tag\", slug, label, tag.description, locale);\n\t\t}\n\t} else if (tags.length > 0) {\n\t\tstate.plan.missingTaxonomies.push(\"tag\");\n\t}\n\n\t// Pass 3: <wp:term> blocks for custom taxonomies (genre, etc.). Skipped:\n\t// - `nav_menu`: menus are handled by `importMenusFromWxr`.\n\t// - `language`: Polylang's locale signal; promoted to `WxrPost.locale`\n\t// by the parser and not a content taxonomy in EmDash.\n\tfor (const term of terms) {\n\t\tif (term.taxonomy === \"nav_menu\" || term.taxonomy === \"language\") continue;\n\t\t// Normalize WordPress' `post_tag` synonym -> EmDash `tag`. WordPress\n\t\t// emits `<wp:tag>` for some exports and `<wp:term wp:term_taxonomy=\"post_tag\">`\n\t\t// for others; both must land in the same EmDash taxonomy.\n\t\tconst taxonomyName = term.taxonomy === \"post_tag\" ? \"tag\" : term.taxonomy;\n\t\tconst def = await lookupDef(taxonomyName);\n\t\tif (!def) {\n\t\t\tif (!state.plan.missingTaxonomies.includes(taxonomyName)) {\n\t\t\t\tstate.plan.missingTaxonomies.push(taxonomyName);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tawait ensureTerm(repo, state, taxonomyName, term.slug, term.name, term.description, locale);\n\t}\n\n\t// Pass 4: per-item assignments. Backfills terms missing from the top-\n\t// level blocks (rare, but observed in hand-edited or partial exports).\n\t// Labels come from the per-item `<category>` text body when the parser\n\t// captured one; otherwise we fall back to the slug. This is the path\n\t// for older exports that skip top-level `<wp:category>` definitions.\n\tlet recordedMissingCategoryFromPosts = false;\n\tlet recordedMissingTagFromPosts = false;\n\tfor (const post of posts) {\n\t\tfor (const slug of post.categories) {\n\t\t\tif (!categoryDef) {\n\t\t\t\tif (\n\t\t\t\t\t!recordedMissingCategoryFromPosts &&\n\t\t\t\t\t!state.plan.missingTaxonomies.includes(\"category\")\n\t\t\t\t) {\n\t\t\t\t\tstate.plan.missingTaxonomies.push(\"category\");\n\t\t\t\t\trecordedMissingCategoryFromPosts = true;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (state.plan.termIdByNameAndSlug.get(\"category\")?.has(slug)) continue;\n\t\t\tawait ensureTerm(\n\t\t\t\trepo,\n\t\t\t\tstate,\n\t\t\t\t\"category\",\n\t\t\t\tslug,\n\t\t\t\tlabelFor(post, \"category\", slug),\n\t\t\t\tundefined,\n\t\t\t\tlocale,\n\t\t\t);\n\t\t}\n\t\tfor (const slug of post.tags) {\n\t\t\tif (!tagDef) {\n\t\t\t\tif (!recordedMissingTagFromPosts && !state.plan.missingTaxonomies.includes(\"tag\")) {\n\t\t\t\t\tstate.plan.missingTaxonomies.push(\"tag\");\n\t\t\t\t\trecordedMissingTagFromPosts = true;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (state.plan.termIdByNameAndSlug.get(\"tag\")?.has(slug)) continue;\n\t\t\tawait ensureTerm(repo, state, \"tag\", slug, labelFor(post, \"tag\", slug), undefined, locale);\n\t\t}\n\t\tif (post.customTaxonomies) {\n\t\t\tfor (const [rawName, slugs] of post.customTaxonomies) {\n\t\t\t\t// `nav_menu` is handled by the menu importer; `language` is\n\t\t\t\t// Polylang's per-post locale signal, already promoted by the\n\t\t\t\t// parser.\n\t\t\t\tif (rawName === \"nav_menu\" || rawName === \"language\") continue;\n\t\t\t\tconst taxonomyName = rawName === \"post_tag\" ? \"tag\" : rawName;\n\t\t\t\tconst def = await lookupDef(taxonomyName);\n\t\t\t\tif (!def) {\n\t\t\t\t\tif (!state.plan.missingTaxonomies.includes(taxonomyName)) {\n\t\t\t\t\t\tstate.plan.missingTaxonomies.push(taxonomyName);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tfor (const slug of slugs) {\n\t\t\t\t\tif (state.plan.termIdByNameAndSlug.get(taxonomyName)?.has(slug)) continue;\n\t\t\t\t\tawait ensureTerm(\n\t\t\t\t\t\trepo,\n\t\t\t\t\t\tstate,\n\t\t\t\t\t\ttaxonomyName,\n\t\t\t\t\t\tslug,\n\t\t\t\t\t\tlabelFor(post, taxonomyName, slug),\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// `content_taxonomies` writes happen later in `attachPostTaxonomies`, but\n\t// term inserts above already invalidate the in-memory \"has any terms\" probe.\n\t// We flush once at the end of the pre-import to keep the runtime cache hot.\n\tinvalidateTermCache();\n\n\treturn state.plan;\n}\n\n/**\n * Walk a parsed WXR post's per-item taxonomy assignments and return only\n * the ones that resolve to a real EmDash term AND aren't filtered out by\n * the taxonomy def's `collections` allowlist. Grouped by EmDash taxonomy\n * name (so `post_tag` is already folded into `tag`). Deduplicated.\n *\n * This is the single source of truth for \"what will the importer try to\n * write for this post\". Both the anchor (additive `attachToEntry`) and\n * translation (per-taxonomy `setTermsForEntry`) paths drive from this map\n * so they agree on which taxonomies need touching. In particular, the\n * translation path uses the keys here -- not `postAssignedTaxonomies` --\n * to decide which inherited pivot rows to clear, so a translation whose\n * only assignment is filtered out by `collections` doesn't lose its\n * inherited terms (see #1087 review feedback).\n *\n * Skipped taxonomies: `nav_menu` (handled by the menu importer) and\n * `language` (Polylang's locale signal, already promoted to `post.locale`\n * by the parser).\n */\nexport function resolvePostTermAssignments(\n\tcollection: string,\n\tpost: WxrPost,\n\tplan: TaxonomyImportPlan,\n): Map<string, string[]> {\n\tconst result = new Map<string, string[]>();\n\tconst seen = new Set<string>();\n\n\tconst tryResolve = (taxonomyName: string, slug: string): void => {\n\t\tconst termId = plan.termIdByNameAndSlug.get(taxonomyName)?.get(slug);\n\t\tif (!termId) return;\n\t\tconst collectionFilter = plan.collectionsByTaxonomy.get(taxonomyName);\n\t\t// Empty set means \"no filter\" (def has no collections array). A\n\t\t// non-empty set is enforced: skip assignments to collections the\n\t\t// def doesn't list. Matches admin UI: a `category` term linked\n\t\t// only to `posts` shouldn't end up on a `products` row just\n\t\t// because the WXR happened to mention it.\n\t\tif (collectionFilter && collectionFilter.size > 0 && !collectionFilter.has(collection)) {\n\t\t\treturn;\n\t\t}\n\t\tconst dedupeKey = `${taxonomyName}\\u0000${termId}`;\n\t\tif (seen.has(dedupeKey)) return;\n\t\tseen.add(dedupeKey);\n\t\tconst existing = result.get(taxonomyName);\n\t\tif (existing) existing.push(termId);\n\t\telse result.set(taxonomyName, [termId]);\n\t};\n\n\tfor (const slug of post.categories) tryResolve(\"category\", slug);\n\tfor (const slug of post.tags) tryResolve(\"tag\", slug);\n\tif (post.customTaxonomies) {\n\t\tfor (const [rawName, slugs] of post.customTaxonomies) {\n\t\t\tif (rawName === \"nav_menu\" || rawName === \"language\") continue;\n\t\t\tconst taxonomyName = rawName === \"post_tag\" ? \"tag\" : rawName;\n\t\t\tfor (const slug of slugs) tryResolve(taxonomyName, slug);\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Attach the taxonomy assignments parsed for a single WXR post to a freshly-\n * created EmDash content row. Additive (`attachToEntry` + `ON CONFLICT DO\n * NOTHING`). Used for anchors -- translations need replace-semantics per\n * taxonomy and should use `setPostTermAssignmentsReplacing` instead.\n *\n * Returns the number of pivot rows actually inserted (excludes rows that\n * already existed via the `ON CONFLICT DO NOTHING` path), so the caller can\n * roll them up into the import summary without over-counting on re-imports.\n */\nexport async function attachPostTaxonomies(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tentryId: string,\n\tpost: WxrPost,\n\tplan: TaxonomyImportPlan,\n): Promise<number> {\n\tconst repo = new TaxonomyRepository(db);\n\tconst resolved = resolvePostTermAssignments(collection, post, plan);\n\n\tlet attached = 0;\n\tfor (const [, termIds] of resolved) {\n\t\tfor (const termId of termIds) {\n\t\t\tconst wrote = await attachToEntryCountingInserts(db, repo, plan, collection, entryId, termId);\n\t\t\tif (wrote) attached++;\n\t\t}\n\t}\n\treturn attached;\n}\n\n/**\n * Replace assignments per-taxonomy from a parsed WXR post. Used for\n * translations: WPML's \"Translate Independently\" mode lets translators\n * override term assignments per-taxonomy, not per-post. A translation that\n * overrides `category` shouldn't lose its inherited `tag` or `genre`. We\n * only call `setTermsForEntry(name, ids)` for taxonomies where the\n * translation actually resolved at least one term -- taxonomies with no\n * resolvable+permitted terms are left alone so inherited rows from\n * `copyEntryTerms` stay intact.\n *\n * Returns the number of pivot rows after replacement (sum of `termIds`\n * lists across taxonomies actually touched). Note this counts logical\n * assignments, not the delta from the prior state; the import summary\n * treats this as an additive count for compatibility with `attachPost-\n * Taxonomies`.\n */\nexport async function setPostTermAssignmentsReplacing(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tentryId: string,\n\tpost: WxrPost,\n\tplan: TaxonomyImportPlan,\n): Promise<number> {\n\tconst repo = new TaxonomyRepository(db);\n\tconst resolved = resolvePostTermAssignments(collection, post, plan);\n\n\tlet attached = 0;\n\tfor (const [taxonomyName, termIds] of resolved) {\n\t\tawait repo.setTermsForEntry(collection, entryId, taxonomyName, termIds);\n\t\tattached += termIds.length;\n\t}\n\treturn attached;\n}\n\n/**\n * Resolve a term id to its `translation_group` (the value\n * `content_taxonomies` stores). Caches the result on the plan so\n * repeated attaches of the same term don't repeat the lookup.\n */\nasync function termTranslationGroup(\n\trepo: TaxonomyRepository,\n\tplan: TaxonomyImportPlan,\n\ttermId: string,\n): Promise<string | null> {\n\tconst cached = plan.translationGroupByTermId.get(termId);\n\tif (cached !== undefined) return cached;\n\tconst term = await repo.findById(termId);\n\tconst group = term?.translationGroup ?? null;\n\tplan.translationGroupByTermId.set(termId, group);\n\treturn group;\n}\n\n/**\n * Wrapper around `TaxonomyRepository.attachToEntry` that returns whether\n * an actual row was inserted (vs. silently skipped by the `ON CONFLICT DO\n * NOTHING` branch). Lets the importer's `assignments` counter reflect real\n * writes rather than re-import no-ops.\n *\n * Best-effort: we check pivot existence first, then call `attachToEntry`.\n * A concurrent insert between the check and the attach would make us\n * report `false` while a row was in fact inserted -- the count is for\n * summary display only, never correctness.\n */\nasync function attachToEntryCountingInserts(\n\tdb: Kysely<Database>,\n\trepo: TaxonomyRepository,\n\tplan: TaxonomyImportPlan,\n\tcollection: string,\n\tentryId: string,\n\ttermId: string,\n): Promise<boolean> {\n\tconst group = await termTranslationGroup(repo, plan, termId);\n\tif (!group) return false;\n\n\tconst existing = await db\n\t\t.selectFrom(\"content_taxonomies\")\n\t\t.select(\"collection\")\n\t\t.where(\"collection\", \"=\", collection)\n\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t.where(\"taxonomy_id\", \"=\", group)\n\t\t.executeTakeFirst();\n\tif (existing) return false;\n\n\tawait repo.attachToEntry(collection, entryId, termId);\n\treturn true;\n}\n\n/**\n * Mirror every term in the plan into each additional locale used by the\n * incoming posts. New rows share the canonical term's `translation_group`\n * so per-locale lookups (`getTermsForEntry(..., locale)`) resolve correctly\n * for translations whose locale differs from the import-wide one.\n *\n * Without this pass, multilingual WXR imports (#1080) write all term rows\n * at the upload-wide locale; the `content_taxonomies` pivot is correct (it\n * stores `translation_group`, not `term id`), but\n * `getTermsForEntry(collection, arabicPostId, \"category\", \"ar\")` filters on\n * `taxonomies.locale = \"ar\"` and returns zero rows. Users see \"no tags\" on\n * every non-canonical translation.\n *\n * Idempotent: skips a locale when a row already exists at `(name, slug,\n * locale)`. Safe to call after `preImportWxrTaxonomies` on subsequent\n * imports.\n */\nexport async function mirrorTermsToLocales(\n\tdb: Kysely<Database>,\n\tplan: TaxonomyImportPlan,\n\tpostLocales: Iterable<string>,\n\tcanonicalLocale: string | undefined,\n): Promise<void> {\n\tconst localeSet = new Set<string>();\n\tfor (const locale of postLocales) {\n\t\tif (!locale || locale === canonicalLocale) continue;\n\t\tlocaleSet.add(locale);\n\t}\n\tif (localeSet.size === 0) return;\n\n\tconst repo = new TaxonomyRepository(db);\n\n\tfor (const [taxonomyName, bySlug] of plan.termIdByNameAndSlug) {\n\t\tfor (const [slug, canonicalTermId] of bySlug) {\n\t\t\t// Resolve the canonical's translation_group once; we'll compare\n\t\t\t// against any pre-existing rows we find at the target locales.\n\t\t\t// Cache on the plan so subsequent attaches (which also need\n\t\t\t// this resolution) don't repeat the lookup.\n\t\t\tconst cachedGroup = await termTranslationGroup(repo, plan, canonicalTermId);\n\t\t\tif (!cachedGroup) {\n\t\t\t\t// The canonical term id is in the plan but the row is no\n\t\t\t\t// longer in the DB. Shouldn't happen during a single\n\t\t\t\t// import run; skip rather than crash so the rest of the\n\t\t\t\t// import can complete.\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst canonicalGroup = cachedGroup;\n\n\t\t\tfor (const locale of localeSet) {\n\t\t\t\tconst existing = await repo.findBySlug(taxonomyName, slug, locale);\n\t\t\t\tif (existing) {\n\t\t\t\t\t// `ensureTerm` resolves cross-locale grouping when it\n\t\t\t\t\t// creates the canonical row, so a pre-existing sibling\n\t\t\t\t\t// row at this locale should already share the\n\t\t\t\t\t// canonical's `translation_group`. If it doesn't, the\n\t\t\t\t\t// import would write pivots pointing at a group that\n\t\t\t\t\t// has no row in this locale -- a silent data-integrity\n\t\t\t\t\t// bug. Fail closed: throw so the operator reconciles\n\t\t\t\t\t// the existing rows in the admin before retrying. This\n\t\t\t\t\t// happens when the canonical row was created in an\n\t\t\t\t\t// earlier import and a sibling-locale row was added\n\t\t\t\t\t// manually afterwards (or vice versa) without linking\n\t\t\t\t\t// them via translationOf.\n\t\t\t\t\tif (existing.translationGroup !== canonicalGroup) {\n\t\t\t\t\t\tthrow new WxrTaxonomyConflictError(\n\t\t\t\t\t\t\t`Cannot import: term \"${taxonomyName}/${slug}\" already exists at locale \"${locale}\" in a different translation group than the canonical row at this import's locale. Reconcile the rows in the admin (re-link via translationOf, or delete one) and retry.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tawait repo.create({\n\t\t\t\t\t\tname: taxonomyName,\n\t\t\t\t\t\tslug,\n\t\t\t\t\t\tlabel: slug, // we don't have a per-locale label from the WXR\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\ttranslationOf: canonicalTermId,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// `findBySlug` + `create` is not atomic. A concurrent\n\t\t\t\t\t// import racing us into the same `(name, slug, locale)`\n\t\t\t\t\t// will trip the UNIQUE constraint. Re-read the row that\n\t\t\t\t\t// won the race and verify its `translation_group`\n\t\t\t\t\t// matches the canonical's; if not, the pivot will\n\t\t\t\t\t// resolve to a group that has no row in this locale\n\t\t\t\t\t// (silent data-integrity bug) so we surface that loudly\n\t\t\t\t\t// rather than continue.\n\t\t\t\t\t//\n\t\t\t\t\t// Other errors (validation, connectivity) re-throw so\n\t\t\t\t\t// the import fails closed rather than silently shipping\n\t\t\t\t\t// translations that resolve to empty taxonomy queries.\n\t\t\t\t\tconst message = error instanceof Error ? error.message.toLowerCase() : \"\";\n\t\t\t\t\tconst isUniqueRace =\n\t\t\t\t\t\tmessage.includes(\"unique constraint failed\") || message.includes(\"duplicate key\");\n\t\t\t\t\tif (!isUniqueRace) throw error;\n\n\t\t\t\t\tconst winner = await repo.findBySlug(taxonomyName, slug, locale);\n\t\t\t\t\tif (!winner) {\n\t\t\t\t\t\t// UNIQUE conflict but no row visible? Shouldn't\n\t\t\t\t\t\t// happen unless the racing transaction rolled back;\n\t\t\t\t\t\t// fail loudly so the operator can investigate.\n\t\t\t\t\t\tthrow new WxrTaxonomyConflictError(\n\t\t\t\t\t\t\t`Cannot import: term \"${taxonomyName}/${slug}\" raced UNIQUE at locale \"${locale}\" but no row is visible afterwards. The concurrent transaction may have rolled back; retry the import.`,\n\t\t\t\t\t\t\t{ cause: error },\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (winner.translationGroup !== canonicalGroup) {\n\t\t\t\t\t\tthrow new WxrTaxonomyConflictError(\n\t\t\t\t\t\t\t`Cannot import: term \"${taxonomyName}/${slug}\" raced UNIQUE at locale \"${locale}\" with a different translation group. Reconcile the rows in the admin and retry.`,\n\t\t\t\t\t\t\t{ cause: error },\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`[WXR import] concurrent writer beat us to term \"${taxonomyName}/${slug}\" at locale \"${locale}\"; using existing row (same group).`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as __exportAll } from "./runner-
|
|
1
|
+
import { a as __exportAll } from "./runner-K8XK101B.mjs";
|
|
2
2
|
import { n as hashString } from "./hash-DrvzIXcz.mjs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
@@ -271,4 +271,4 @@ function uniqueInterfaceNames(collections) {
|
|
|
271
271
|
|
|
272
272
|
//#endregion
|
|
273
273
|
export { zod_generator_exports as i, generateZodSchema as n, uniqueInterfaceNames as r, generateTypeScript as t };
|
|
274
|
-
//# sourceMappingURL=zod-generator-
|
|
274
|
+
//# sourceMappingURL=zod-generator-CacrR-i8.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zod-generator-gFsRuNNy.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\t// Accept every value that legitimately round-trips through the admin\n\t\t\t// and seeds: ISO with `Z`, ISO with a timezone offset, a naive\n\t\t\t// datetime (`YYYY-MM-DDTHH:mm[:ss]` -- what `<input type=\"datetime-local\">`\n\t\t\t// and many seeds produce), and a date-only value. The admin re-sends\n\t\t\t// every loaded field on autosave, so a stored naive datetime must\n\t\t\t// validate or the entry becomes unsavable through its own editor\n\t\t\t// (#1368; same class as #867). `z.iso.*` retains semantic validation,\n\t\t\t// so impossible dates are still rejected.\n\t\t\treturn z.iso.datetime({ offset: true, local: true }).or(z.iso.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-CacrR-i8.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\t// Accept every value that legitimately round-trips through the admin\n\t\t\t// and seeds: ISO with `Z`, ISO with a timezone offset, a naive\n\t\t\t// datetime (`YYYY-MM-DDTHH:mm[:ss]` -- what `<input type=\"datetime-local\">`\n\t\t\t// and many seeds produce), and a date-only value. The admin re-sends\n\t\t\t// every loaded field on autosave, so a stored naive datetime must\n\t\t\t// validate or the entry becomes unsavable through its own editor\n\t\t\t// (#1368; same class as #867). `z.iso.*` retains semantic validation,\n\t\t\t// so impossible dates are still rejected.\n\t\t\treturn z.iso.datetime({ offset: true, local: true }).or(z.iso.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.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Astro-native CMS with WordPress migration support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -230,9 +230,9 @@
|
|
|
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.
|
|
234
|
-
"@emdash-cms/auth": "0.
|
|
235
|
-
"@emdash-cms/gutenberg-to-portable-text": "0.
|
|
233
|
+
"@emdash-cms/admin": "0.28.0",
|
|
234
|
+
"@emdash-cms/auth": "0.28.0",
|
|
235
|
+
"@emdash-cms/gutenberg-to-portable-text": "0.28.0",
|
|
236
236
|
"@emdash-cms/plugin-types": "0.1.0",
|
|
237
237
|
"@emdash-cms/registry-client": "0.3.2"
|
|
238
238
|
},
|
|
@@ -260,14 +260,14 @@
|
|
|
260
260
|
"@types/react": "19.2.14",
|
|
261
261
|
"@types/sanitize-html": "^2.16.0",
|
|
262
262
|
"@types/sax": "^1.2.7",
|
|
263
|
-
"@vitest/ui": "^4.1.
|
|
263
|
+
"@vitest/ui": "^4.1.9",
|
|
264
264
|
"publint": "0.3.17",
|
|
265
265
|
"tsdown": "0.20.3",
|
|
266
266
|
"typescript": "^6.0.3",
|
|
267
267
|
"vite": "^6.0.0",
|
|
268
268
|
"vitest": "^4.1.5",
|
|
269
269
|
"zod-openapi": "^5.4.6",
|
|
270
|
-
"@emdash-cms/blocks": "0.
|
|
270
|
+
"@emdash-cms/blocks": "0.28.0"
|
|
271
271
|
},
|
|
272
272
|
"repository": {
|
|
273
273
|
"type": "git",
|
|
@@ -327,6 +327,36 @@ async function resolveSearchColumns(db: Kysely<Database>, collection: string): P
|
|
|
327
327
|
return columns;
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Create a 301 auto-redirect from an entry's old URL to its new one after a
|
|
332
|
+
* slug change, using the collection's URL pattern. Shared by
|
|
333
|
+
* handleContentUpdate (direct slug edits) and handleContentPublish (slug edits
|
|
334
|
+
* staged as `_slug` in a draft revision, which only land on publish).
|
|
335
|
+
*/
|
|
336
|
+
async function createSlugChangeRedirect(
|
|
337
|
+
db: Kysely<Database>,
|
|
338
|
+
collection: string,
|
|
339
|
+
oldSlug: string,
|
|
340
|
+
newSlug: string,
|
|
341
|
+
contentId: string,
|
|
342
|
+
): Promise<void> {
|
|
343
|
+
const collectionRow = await db
|
|
344
|
+
.selectFrom("_emdash_collections")
|
|
345
|
+
.select("url_pattern")
|
|
346
|
+
.where("slug", "=", collection)
|
|
347
|
+
.executeTakeFirst();
|
|
348
|
+
|
|
349
|
+
const redirectRepo = new RedirectRepository(db);
|
|
350
|
+
await redirectRepo.createAutoRedirect(
|
|
351
|
+
collection,
|
|
352
|
+
oldSlug,
|
|
353
|
+
newSlug,
|
|
354
|
+
contentId,
|
|
355
|
+
collectionRow?.url_pattern ?? null,
|
|
356
|
+
);
|
|
357
|
+
invalidateRedirectCache();
|
|
358
|
+
}
|
|
359
|
+
|
|
330
360
|
/** Matches a date-only `YYYY-MM-DD` bound (no time component). */
|
|
331
361
|
const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
332
362
|
|
|
@@ -865,21 +895,7 @@ export async function handleContentUpdate(
|
|
|
865
895
|
|
|
866
896
|
// Create auto-redirect when slug changes
|
|
867
897
|
if (oldSlug && body.slug) {
|
|
868
|
-
|
|
869
|
-
.selectFrom("_emdash_collections")
|
|
870
|
-
.select("url_pattern")
|
|
871
|
-
.where("slug", "=", collection)
|
|
872
|
-
.executeTakeFirst();
|
|
873
|
-
|
|
874
|
-
const redirectRepo = new RedirectRepository(trx);
|
|
875
|
-
await redirectRepo.createAutoRedirect(
|
|
876
|
-
collection,
|
|
877
|
-
oldSlug,
|
|
878
|
-
body.slug,
|
|
879
|
-
resolvedId,
|
|
880
|
-
collectionRow?.url_pattern ?? null,
|
|
881
|
-
);
|
|
882
|
-
invalidateRedirectCache();
|
|
898
|
+
await createSlugChangeRedirect(trx, collection, oldSlug, body.slug, resolvedId);
|
|
883
899
|
}
|
|
884
900
|
|
|
885
901
|
// Sync non-translatable fields to sibling locales in the same
|
|
@@ -1046,15 +1062,18 @@ export async function handleContentDelete(
|
|
|
1046
1062
|
db: Kysely<Database>,
|
|
1047
1063
|
collection: string,
|
|
1048
1064
|
id: string,
|
|
1049
|
-
): Promise<ApiResult<{ deleted: true }>> {
|
|
1065
|
+
): Promise<ApiResult<{ deleted: true; id: string }>> {
|
|
1050
1066
|
try {
|
|
1051
|
-
const
|
|
1067
|
+
const result = await withTransaction(db, async (trx) => {
|
|
1052
1068
|
const repo = new ContentRepository(trx);
|
|
1053
1069
|
const resolvedId = (await resolveId(repo, collection, id)) ?? id;
|
|
1054
|
-
return
|
|
1070
|
+
return {
|
|
1071
|
+
id: resolvedId,
|
|
1072
|
+
deleted: await repo.delete(collection, resolvedId),
|
|
1073
|
+
};
|
|
1055
1074
|
});
|
|
1056
1075
|
|
|
1057
|
-
if (!deleted) {
|
|
1076
|
+
if (!result.deleted) {
|
|
1058
1077
|
return {
|
|
1059
1078
|
success: false,
|
|
1060
1079
|
error: {
|
|
@@ -1066,7 +1085,7 @@ export async function handleContentDelete(
|
|
|
1066
1085
|
|
|
1067
1086
|
return {
|
|
1068
1087
|
success: true,
|
|
1069
|
-
data: { deleted: true },
|
|
1088
|
+
data: { deleted: true, id: result.id },
|
|
1070
1089
|
};
|
|
1071
1090
|
} catch (error) {
|
|
1072
1091
|
console.error("Content delete error:", error);
|
|
@@ -1129,7 +1148,7 @@ export async function handleContentPermanentDelete(
|
|
|
1129
1148
|
db: Kysely<Database>,
|
|
1130
1149
|
collection: string,
|
|
1131
1150
|
id: string,
|
|
1132
|
-
): Promise<ApiResult<{ deleted: true }>> {
|
|
1151
|
+
): Promise<ApiResult<{ deleted: true; id: string }>> {
|
|
1133
1152
|
try {
|
|
1134
1153
|
const repo = new ContentRepository(db);
|
|
1135
1154
|
const resolvedId = (await resolveIdIncludingTrashed(repo, collection, id)) ?? id;
|
|
@@ -1166,7 +1185,7 @@ export async function handleContentPermanentDelete(
|
|
|
1166
1185
|
|
|
1167
1186
|
return {
|
|
1168
1187
|
success: true,
|
|
1169
|
-
data: { deleted: true },
|
|
1188
|
+
data: { deleted: true, id: resolvedId },
|
|
1170
1189
|
};
|
|
1171
1190
|
} catch (error) {
|
|
1172
1191
|
console.error("Content permanent delete error:", error);
|
|
@@ -1362,7 +1381,34 @@ export async function handleContentPublish(
|
|
|
1362
1381
|
const item = await withTransaction(db, async (trx) => {
|
|
1363
1382
|
const repo = new ContentRepository(trx);
|
|
1364
1383
|
const resolvedId = (await resolveId(repo, collection, id)) ?? id;
|
|
1365
|
-
|
|
1384
|
+
|
|
1385
|
+
// Capture the pre-publish state. For revision-supporting collections a
|
|
1386
|
+
// slug edit is staged as `_slug` in the draft revision and only lands
|
|
1387
|
+
// on the live `slug` column here, inside `repo.publish()` — it never
|
|
1388
|
+
// passes through handleContentUpdate, where slug-change auto-redirects
|
|
1389
|
+
// are normally created.
|
|
1390
|
+
const existing = await repo.findById(collection, resolvedId);
|
|
1391
|
+
|
|
1392
|
+
const published = await repo.publish(
|
|
1393
|
+
collection,
|
|
1394
|
+
resolvedId,
|
|
1395
|
+
options.publishedAt,
|
|
1396
|
+
options.requireScheduledDue,
|
|
1397
|
+
);
|
|
1398
|
+
|
|
1399
|
+
// Leave a 301 behind when publishing changed the slug of an entry that
|
|
1400
|
+
// was already published — its old URL was live and may be indexed or
|
|
1401
|
+
// linked. A first publish is excluded: a draft's URL was never public.
|
|
1402
|
+
if (
|
|
1403
|
+
existing?.status === "published" &&
|
|
1404
|
+
existing.slug &&
|
|
1405
|
+
published.slug &&
|
|
1406
|
+
existing.slug !== published.slug
|
|
1407
|
+
) {
|
|
1408
|
+
await createSlugChangeRedirect(trx, collection, existing.slug, published.slug, resolvedId);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
return published;
|
|
1366
1412
|
});
|
|
1367
1413
|
|
|
1368
1414
|
const hasSeo = await collectionHasSeo(db, collection);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { Kysely } from "kysely";
|
|
6
6
|
|
|
7
7
|
import type { Database } from "../../database/types.js";
|
|
8
|
+
import type { SandboxedPluginEntry } from "../../emdash-runtime.js";
|
|
8
9
|
import { PluginStateRepository, type PluginState, type PluginStatus } from "../../plugins/state.js";
|
|
9
10
|
import type { ResolvedPlugin } from "../../plugins/types.js";
|
|
10
11
|
import type { ApiResult } from "../types.js";
|
|
@@ -17,6 +18,8 @@ export interface PluginInfo {
|
|
|
17
18
|
enabled: boolean;
|
|
18
19
|
status: PluginStatus;
|
|
19
20
|
source?: "config" | "marketplace" | "registry";
|
|
21
|
+
/** True for statically-sandboxed plugins (registered via `sandboxed: []`) */
|
|
22
|
+
sandboxed?: boolean;
|
|
20
23
|
marketplaceVersion?: string;
|
|
21
24
|
/** Publisher DID, for registry-source plugins */
|
|
22
25
|
registryPublisherDid?: string;
|
|
@@ -84,12 +87,43 @@ function buildPluginInfo(
|
|
|
84
87
|
};
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Build plugin info for a statically-sandboxed plugin entry
|
|
92
|
+
*/
|
|
93
|
+
function buildSandboxedPluginInfo(
|
|
94
|
+
entry: SandboxedPluginEntry,
|
|
95
|
+
state: PluginState | null,
|
|
96
|
+
): PluginInfo {
|
|
97
|
+
const status = state?.status ?? "active";
|
|
98
|
+
const enabled = status === "active";
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
id: entry.id,
|
|
102
|
+
name: state?.displayName || entry.id,
|
|
103
|
+
version: entry.version,
|
|
104
|
+
package: undefined, // v2 doesn't have package field
|
|
105
|
+
enabled,
|
|
106
|
+
status,
|
|
107
|
+
source: "config",
|
|
108
|
+
sandboxed: true,
|
|
109
|
+
capabilities: entry.capabilities,
|
|
110
|
+
hasAdminPages: (entry.adminPages?.length ?? 0) > 0,
|
|
111
|
+
hasDashboardWidgets: (entry.adminWidgets?.length ?? 0) > 0,
|
|
112
|
+
hasHooks: false,
|
|
113
|
+
installedAt: state?.installedAt?.toISOString(),
|
|
114
|
+
activatedAt: state?.activatedAt?.toISOString() ?? undefined,
|
|
115
|
+
deactivatedAt: state?.deactivatedAt?.toISOString() ?? undefined,
|
|
116
|
+
description: state?.description ?? undefined,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
87
120
|
/**
|
|
88
121
|
* List all configured plugins with their state
|
|
89
122
|
*/
|
|
90
123
|
export async function handlePluginList(
|
|
91
124
|
db: Kysely<Database>,
|
|
92
125
|
configuredPlugins: ResolvedPlugin[],
|
|
126
|
+
sandboxedPluginEntries: SandboxedPluginEntry[],
|
|
93
127
|
marketplaceUrl?: string,
|
|
94
128
|
): Promise<ApiResult<PluginListResponse>> {
|
|
95
129
|
try {
|
|
@@ -104,6 +138,14 @@ export async function handlePluginList(
|
|
|
104
138
|
return buildPluginInfo(plugin, state, marketplaceUrl);
|
|
105
139
|
});
|
|
106
140
|
|
|
141
|
+
// Include statically-sandboxed plugins (registered via `sandboxed: []`
|
|
142
|
+
// in astro.config.mjs).
|
|
143
|
+
for (const entry of sandboxedPluginEntries) {
|
|
144
|
+
if (configuredIds.has(entry.id)) continue;
|
|
145
|
+
configuredIds.add(entry.id);
|
|
146
|
+
items.push(buildSandboxedPluginInfo(entry, stateMap.get(entry.id) ?? null));
|
|
147
|
+
}
|
|
148
|
+
|
|
107
149
|
// Include runtime-installed plugins (marketplace or registry) that
|
|
108
150
|
// aren't in the configured plugins list.
|
|
109
151
|
for (const state of allStates) {
|
|
@@ -156,27 +198,37 @@ export async function handlePluginList(
|
|
|
156
198
|
export async function handlePluginGet(
|
|
157
199
|
db: Kysely<Database>,
|
|
158
200
|
configuredPlugins: ResolvedPlugin[],
|
|
201
|
+
sandboxedPluginEntries: SandboxedPluginEntry[],
|
|
159
202
|
pluginId: string,
|
|
160
203
|
marketplaceUrl?: string,
|
|
161
204
|
): Promise<ApiResult<PluginResponse>> {
|
|
162
205
|
try {
|
|
206
|
+
const stateRepo = new PluginStateRepository(db);
|
|
163
207
|
const plugin = configuredPlugins.find((p) => p.id === pluginId);
|
|
164
|
-
|
|
208
|
+
|
|
209
|
+
if (plugin) {
|
|
210
|
+
const state = await stateRepo.get(pluginId);
|
|
165
211
|
return {
|
|
166
|
-
success:
|
|
167
|
-
|
|
168
|
-
code: "NOT_FOUND",
|
|
169
|
-
message: `Plugin not found: ${pluginId}`,
|
|
170
|
-
},
|
|
212
|
+
success: true,
|
|
213
|
+
data: { item: buildPluginInfo(plugin, state, marketplaceUrl) },
|
|
171
214
|
};
|
|
172
215
|
}
|
|
173
216
|
|
|
174
|
-
const
|
|
175
|
-
|
|
217
|
+
const sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);
|
|
218
|
+
if (sandboxed) {
|
|
219
|
+
const state = await stateRepo.get(pluginId);
|
|
220
|
+
return {
|
|
221
|
+
success: true,
|
|
222
|
+
data: { item: buildSandboxedPluginInfo(sandboxed, state) },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
176
225
|
|
|
177
226
|
return {
|
|
178
|
-
success:
|
|
179
|
-
|
|
227
|
+
success: false,
|
|
228
|
+
error: {
|
|
229
|
+
code: "NOT_FOUND",
|
|
230
|
+
message: `Plugin not found: ${pluginId}`,
|
|
231
|
+
},
|
|
180
232
|
};
|
|
181
233
|
} catch {
|
|
182
234
|
return {
|
|
@@ -227,6 +279,7 @@ function buildStateOnlyPluginInfo(
|
|
|
227
279
|
export async function handlePluginEnable(
|
|
228
280
|
db: Kysely<Database>,
|
|
229
281
|
configuredPlugins: ResolvedPlugin[],
|
|
282
|
+
sandboxedPluginEntries: SandboxedPluginEntry[],
|
|
230
283
|
pluginId: string,
|
|
231
284
|
): Promise<ApiResult<PluginResponse>> {
|
|
232
285
|
try {
|
|
@@ -239,6 +292,13 @@ export async function handlePluginEnable(
|
|
|
239
292
|
return { success: true, data: { item: buildPluginInfo(plugin, state) } };
|
|
240
293
|
}
|
|
241
294
|
|
|
295
|
+
// Statically-sandboxed plugin: addressable via its build-time entry.
|
|
296
|
+
const sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);
|
|
297
|
+
if (sandboxed) {
|
|
298
|
+
const state = await stateRepo.enable(pluginId, sandboxed.version);
|
|
299
|
+
return { success: true, data: { item: buildSandboxedPluginInfo(sandboxed, state) } };
|
|
300
|
+
}
|
|
301
|
+
|
|
242
302
|
// Runtime-installed plugin (marketplace or registry): only
|
|
243
303
|
// addressable through the state row. Fall back to the existing
|
|
244
304
|
// version recorded there.
|
|
@@ -268,6 +328,7 @@ export async function handlePluginEnable(
|
|
|
268
328
|
export async function handlePluginDisable(
|
|
269
329
|
db: Kysely<Database>,
|
|
270
330
|
configuredPlugins: ResolvedPlugin[],
|
|
331
|
+
sandboxedPluginEntries: SandboxedPluginEntry[],
|
|
271
332
|
pluginId: string,
|
|
272
333
|
): Promise<ApiResult<PluginResponse>> {
|
|
273
334
|
try {
|
|
@@ -279,6 +340,12 @@ export async function handlePluginDisable(
|
|
|
279
340
|
return { success: true, data: { item: buildPluginInfo(plugin, state) } };
|
|
280
341
|
}
|
|
281
342
|
|
|
343
|
+
const sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);
|
|
344
|
+
if (sandboxed) {
|
|
345
|
+
const state = await stateRepo.disable(pluginId, sandboxed.version);
|
|
346
|
+
return { success: true, data: { item: buildSandboxedPluginInfo(sandboxed, state) } };
|
|
347
|
+
}
|
|
348
|
+
|
|
282
349
|
const existing = await stateRepo.get(pluginId);
|
|
283
350
|
if (!existing || (existing.source !== "marketplace" && existing.source !== "registry")) {
|
|
284
351
|
return {
|
|
@@ -19,6 +19,25 @@ export const wpPluginExecuteBody = z.object({
|
|
|
19
19
|
url: httpUrl,
|
|
20
20
|
token: z.string().min(1),
|
|
21
21
|
config: z.record(z.string(), z.unknown()),
|
|
22
|
+
// --- Chunked mode (issue #475). Absent phase = single-shot legacy run. ---
|
|
23
|
+
// The admin drives the import in a loop of bounded requests; cross-chunk
|
|
24
|
+
// state travels with the client so the server stays stateless.
|
|
25
|
+
phase: z.enum(["content", "comments", "finalize"]).optional(),
|
|
26
|
+
/** Position within the phase: post-type index + WP page (content), page (comments) */
|
|
27
|
+
cursor: z
|
|
28
|
+
.object({
|
|
29
|
+
postTypeIndex: z.number().int().min(0).default(0),
|
|
30
|
+
page: z.number().int().min(1).default(1),
|
|
31
|
+
})
|
|
32
|
+
.optional(),
|
|
33
|
+
/** WP post ID -> created EmDash item, accumulated across content chunks */
|
|
34
|
+
idMap: z
|
|
35
|
+
.record(z.string(), z.object({ id: z.string().min(1), collection: z.string().min(1) }))
|
|
36
|
+
.optional(),
|
|
37
|
+
/** Source translation group -> EmDash item ID, accumulated across content chunks */
|
|
38
|
+
translationGroups: z.record(z.string(), z.string().min(1)).optional(),
|
|
39
|
+
/** WP comment ID -> EmDash root comment ID, accumulated across comment chunks */
|
|
40
|
+
commentRoots: z.record(z.string(), z.string().min(1)).optional(),
|
|
22
41
|
});
|
|
23
42
|
|
|
24
43
|
export const wpPrepareBody = z.object({
|
|
@@ -220,14 +220,11 @@ export interface EmDashConfig {
|
|
|
220
220
|
*
|
|
221
221
|
* @example
|
|
222
222
|
* ```ts
|
|
223
|
-
* import
|
|
224
|
-
* import
|
|
223
|
+
* import auditLog from "@emdash-cms/plugin-audit-log";
|
|
224
|
+
* import webhookNotifier from "@emdash-cms/plugin-webhook-notifier";
|
|
225
225
|
*
|
|
226
226
|
* emdash({
|
|
227
|
-
* plugins: [
|
|
228
|
-
* auditLogPlugin(),
|
|
229
|
-
* webhookNotifierPlugin({ url: "https://example.com/webhook" }),
|
|
230
|
-
* ],
|
|
227
|
+
* plugins: [auditLog, webhookNotifier],
|
|
231
228
|
* })
|
|
232
229
|
* ```
|
|
233
230
|
*/
|