emdash 0.8.0 → 0.10.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-BKSf3T9R.d.mts → adapters-BktHA7EO.d.mts} +1 -1
- package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
- package/dist/{apply-x0eMK1lX.mjs → apply-UsrFuO7l.mjs} +207 -355
- package/dist/apply-UsrFuO7l.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +118 -4
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +6 -7
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +14 -57
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +15 -10
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +8 -5
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +70 -121
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +25 -10
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-Chbr2GoP.mjs → byline-C3vnhIpU.mjs} +4 -4
- package/dist/{byline-Chbr2GoP.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
- package/dist/bylines-esI7ioa9.mjs +113 -0
- package/dist/bylines-esI7ioa9.mjs.map +1 -0
- package/dist/cache-fTzxgMFJ.mjs +65 -0
- package/dist/cache-fTzxgMFJ.mjs.map +1 -0
- package/dist/{chunks-HGz06Soa.mjs → chunks-Da2-b-oA.mjs} +8 -2
- package/dist/{chunks-HGz06Soa.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
- package/dist/cli/index.mjs +456 -90
- 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 +3 -3
- package/dist/client/index.mjs.map +1 -1
- package/dist/{config-BXwuX8Bx.mjs → config-CVssduLe.mjs} +1 -1
- package/dist/{config-BXwuX8Bx.mjs.map → config-CVssduLe.mjs.map} +1 -1
- package/dist/{content-BcQPYxdV.mjs → content-C7G4QXkK.mjs} +42 -14
- package/dist/content-C7G4QXkK.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.d.mts.map +1 -1
- package/dist/db/libsql.mjs +7 -2
- package/dist/db/libsql.mjs.map +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.d.mts.map +1 -1
- package/dist/db/sqlite.mjs +8 -3
- package/dist/db/sqlite.mjs.map +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs → db-errors-B7P2pSCn.mjs} +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
- package/dist/{default-DCVqE5ib.mjs → default-pHuz9WF6.mjs} +1 -1
- package/dist/{default-DCVqE5ib.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
- package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
- package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
- package/dist/{error-zG5T1UGA.mjs → error-DqnRMM5z.mjs} +1 -1
- package/dist/{error-zG5T1UGA.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
- package/dist/{index-DIb-CzNx.d.mts → index-DjPMOfO0.d.mts} +162 -87
- package/dist/index-DjPMOfO0.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +27 -24
- package/dist/{load-CyEoextb.mjs → load-sXRuM7Us.mjs} +2 -2
- package/dist/{load-CyEoextb.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
- package/dist/{loader-CndGj8kM.mjs → loader-Bx2_9-5e.mjs} +53 -8
- package/dist/loader-Bx2_9-5e.mjs.map +1 -0
- package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-CXAbd1vH.mjs} +33 -3
- package/dist/manifest-schema-CXAbd1vH.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-BnAOqItE.mjs → mode-YhqNVef_.mjs} +1 -1
- package/dist/{mode-BnAOqItE.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
- package/dist/options-nPxWnrya.mjs +117 -0
- package/dist/options-nPxWnrya.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
- package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/public-url-B1AxbbbQ.mjs +51 -0
- package/dist/public-url-B1AxbbbQ.mjs.map +1 -0
- package/dist/{query-fqEdLFms.mjs → query-Bo-msrmu.mjs} +114 -16
- package/dist/query-Bo-msrmu.mjs.map +1 -0
- package/dist/{redirect-D_pshWdf.mjs → redirect-C5H7VGIX.mjs} +11 -6
- package/dist/redirect-C5H7VGIX.mjs.map +1 -0
- package/dist/{registry-C3Mr0ODu.mjs → registry-Beb7wxFc.mjs} +39 -5
- package/dist/registry-Beb7wxFc.mjs.map +1 -0
- package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-C-tIpYIw.mjs} +1 -1
- package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
- package/dist/runner-Clwe4Mme.d.mts +44 -0
- package/dist/runner-Clwe4Mme.d.mts.map +1 -0
- package/dist/{runner-tQ7BJ4T7.mjs → runner-DMnlIkh4.mjs} +616 -191
- package/dist/runner-DMnlIkh4.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BoZYFuUk.mjs → search-DkN-BqsS.mjs} +270 -152
- package/dist/search-DkN-BqsS.mjs.map +1 -0
- package/dist/secrets-CZ8rxLX3.mjs +314 -0
- package/dist/secrets-CZ8rxLX3.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +13 -11
- package/dist/seo/index.d.mts +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-CTtewrSQ.mjs +407 -0
- package/dist/taxonomies-CTtewrSQ.mjs.map +1 -0
- package/dist/taxonomy-DSxx2K2L.mjs +218 -0
- package/dist/taxonomy-DSxx2K2L.mjs.map +1 -0
- package/dist/{tokens-D9vnZqYS.mjs → tokens-CyRDPVW2.mjs} +1 -1
- package/dist/{tokens-D9vnZqYS.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts → transport-DX_5rpsq.d.mts} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
- package/dist/{transport-C9ugt2Nr.mjs → transport-xpzIjCIB.mjs} +6 -5
- package/dist/{transport-C9ugt2Nr.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
- package/dist/{types-BrA0xf5I.d.mts → types-B_CXXnzh.d.mts} +1 -1
- package/dist/{types-BrA0xf5I.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
- package/dist/{types-DIMwPFub.d.mts → types-C-aFbqmA.d.mts} +1 -1
- package/dist/{types-DIMwPFub.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
- package/dist/types-CoO6mpV3.mjs +68 -0
- package/dist/types-CoO6mpV3.mjs.map +1 -0
- package/dist/{types-i36XcA_X.d.mts → types-D19uBYWn.d.mts} +83 -7
- package/dist/types-D19uBYWn.d.mts.map +1 -0
- package/dist/{types-BmPPSUEx.d.mts → types-Dl1fgFjn.d.mts} +24 -2
- package/dist/{types-BmPPSUEx.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
- package/dist/{types-CS8FIX7L.d.mts → types-Dtx1mSMX.d.mts} +9 -1
- package/dist/types-Dtx1mSMX.d.mts.map +1 -0
- package/dist/{types-Bm1dn-q3.mjs → types-Eg829jj9.mjs} +1 -1
- package/dist/{types-Bm1dn-q3.mjs.map → types-Eg829jj9.mjs.map} +1 -1
- package/dist/{types-CgqmmMJB.mjs → types-K-EkEQCI.mjs} +1 -1
- package/dist/{types-CgqmmMJB.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
- package/dist/{validate-CxVsLehf.mjs → validate-CBIbxM3L.mjs} +14 -10
- package/dist/validate-CBIbxM3L.mjs.map +1 -0
- package/dist/{validate-DHxmpFJt.d.mts → validate-DHGwADqO.d.mts} +18 -5
- package/dist/validate-DHGwADqO.d.mts.map +1 -0
- package/dist/{validation-C-ZpN2GI.mjs → validation-B1NYiEos.mjs} +6 -6
- package/dist/{validation-C-ZpN2GI.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
- package/dist/version-CMD42IRC.mjs +7 -0
- package/dist/{version-Bbq8TCrz.mjs.map → version-CMD42IRC.mjs.map} +1 -1
- package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-BNJDQBSZ.mjs} +11 -6
- package/dist/{zod-generator-CpwccCIv.mjs.map → zod-generator-BNJDQBSZ.mjs.map} +1 -1
- package/locals.d.ts +1 -6
- package/package.json +9 -8
- package/src/api/handlers/comments.ts +6 -4
- package/src/api/handlers/content.ts +40 -1
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/device-flow.ts +5 -0
- package/src/api/handlers/marketplace.ts +11 -4
- package/src/api/handlers/menus.ts +256 -75
- package/src/api/handlers/oauth-authorization.ts +72 -33
- package/src/api/handlers/revision.ts +23 -14
- package/src/api/handlers/taxonomies.ts +273 -100
- package/src/api/public-url.ts +48 -2
- package/src/api/schemas/comments.ts +2 -2
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/content.ts +17 -0
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/sections.ts +3 -3
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/api/schemas/users.ts +1 -1
- package/src/api/types.ts +5 -1
- package/src/astro/integration/index.ts +17 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/integration/runtime.ts +30 -0
- package/src/astro/integration/virtual-modules.ts +32 -2
- package/src/astro/integration/vite-config.ts +6 -1
- package/src/astro/middleware/auth.ts +13 -6
- package/src/astro/middleware/redirect.ts +29 -16
- package/src/astro/middleware/request-context.ts +15 -5
- package/src/astro/middleware.ts +23 -9
- package/src/astro/routes/api/auth/invite/complete.ts +6 -1
- package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
- package/src/astro/routes/api/auth/signup/complete.ts +6 -1
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
- package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
- package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
- package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
- package/src/astro/routes/api/manifest.ts +62 -45
- package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
- package/src/astro/routes/api/menus/[name]/items.ts +16 -6
- package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
- package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
- package/src/astro/routes/api/menus/[name].ts +19 -10
- package/src/astro/routes/api/menus/index.ts +9 -6
- package/src/astro/routes/api/openapi.json.ts +27 -10
- package/src/astro/routes/api/redirects/404s/index.ts +10 -4
- package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
- package/src/astro/routes/api/redirects/[id].ts +10 -4
- package/src/astro/routes/api/redirects/index.ts +7 -3
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
- package/src/astro/routes/api/schema/collections/index.ts +1 -1
- package/src/astro/routes/api/search/index.ts +10 -2
- package/src/astro/routes/api/sections/[slug].ts +10 -4
- package/src/astro/routes/api/sections/index.ts +7 -3
- package/src/astro/routes/api/setup/admin-verify.ts +6 -1
- package/src/astro/routes/api/snapshot.ts +44 -18
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
- package/src/astro/routes/api/taxonomies/index.ts +9 -7
- package/src/astro/routes/api/themes/preview.ts +11 -5
- package/src/astro/types.ts +23 -3
- package/src/auth/allowed-origins.ts +168 -0
- package/src/auth/passkey-config.ts +35 -13
- package/src/bylines/index.ts +37 -88
- package/src/cli/commands/auth.ts +28 -6
- package/src/cli/commands/bundle-utils.ts +11 -2
- package/src/cli/commands/bundle.ts +28 -8
- package/src/cli/commands/content.ts +13 -0
- package/src/cli/commands/export-seed.ts +82 -21
- package/src/cli/commands/login.ts +8 -1
- package/src/cli/commands/plugin-init.ts +216 -90
- package/src/cli/commands/publish.ts +24 -0
- package/src/cli/commands/secrets.ts +183 -0
- package/src/cli/credentials.ts +1 -1
- package/src/cli/index.ts +5 -1
- package/src/client/index.ts +4 -4
- package/src/client/transport.ts +17 -7
- package/src/components/Break.astro +2 -2
- package/src/components/EmDashHead.astro +18 -13
- package/src/components/Embed.astro +1 -1
- package/src/components/Gallery.astro +1 -1
- package/src/components/Image.astro +1 -1
- package/src/components/InlinePortableTextEditor.tsx +104 -18
- package/src/config/secrets.ts +528 -0
- package/src/database/dialect-helpers.ts +50 -0
- package/src/database/migrations/034_published_at_index.ts +1 -1
- package/src/database/migrations/035_bounded_404_log.ts +56 -39
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/runner.ts +158 -23
- package/src/database/repositories/content.ts +47 -12
- package/src/database/repositories/redirect.ts +14 -3
- package/src/database/repositories/taxonomy.ts +212 -82
- package/src/database/types.ts +10 -2
- package/src/db/libsql.ts +1 -3
- package/src/db/sqlite.ts +2 -5
- package/src/emdash-runtime.ts +84 -159
- package/src/i18n/resolve.ts +37 -0
- package/src/index.ts +9 -0
- package/src/loader.ts +73 -3
- package/src/mcp/server.ts +180 -54
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- package/src/page/site-identity.ts +58 -0
- package/src/plugins/adapt-sandbox-entry.ts +22 -10
- package/src/plugins/context.ts +13 -10
- package/src/plugins/define-plugin.ts +40 -12
- package/src/plugins/hooks.ts +23 -19
- package/src/plugins/index.ts +9 -0
- package/src/plugins/manifest-schema.ts +37 -2
- package/src/plugins/types.ts +151 -11
- package/src/preview/urls.ts +23 -3
- package/src/query.ts +148 -5
- package/src/redirects/cache.ts +38 -18
- package/src/schema/registry.ts +56 -0
- package/src/schema/zod-generator.ts +39 -7
- package/src/seed/apply.ts +142 -54
- package/src/seed/types.ts +14 -1
- package/src/seed/validate.ts +27 -13
- package/src/settings/index.ts +80 -6
- package/src/settings/types.ts +23 -1
- package/src/taxonomies/index.ts +237 -210
- package/src/taxonomies/types.ts +10 -0
- package/dist/apply-x0eMK1lX.mjs.map +0 -1
- package/dist/bylines-CRNsVG88.mjs +0 -157
- package/dist/bylines-CRNsVG88.mjs.map +0 -1
- package/dist/cache-BkKBuIvS.mjs +0 -56
- package/dist/cache-BkKBuIvS.mjs.map +0 -1
- package/dist/chunk-ClPoSABd.mjs +0 -21
- package/dist/content-BcQPYxdV.mjs.map +0 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
- package/dist/index-DIb-CzNx.d.mts.map +0 -1
- package/dist/loader-CndGj8kM.mjs.map +0 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
- package/dist/query-fqEdLFms.mjs.map +0 -1
- package/dist/redirect-D_pshWdf.mjs.map +0 -1
- package/dist/registry-C3Mr0ODu.mjs.map +0 -1
- package/dist/runner-OURCaApa.d.mts +0 -34
- package/dist/runner-OURCaApa.d.mts.map +0 -1
- package/dist/runner-tQ7BJ4T7.mjs.map +0 -1
- package/dist/search-BoZYFuUk.mjs.map +0 -1
- package/dist/taxonomies-B4IAshV8.mjs +0 -308
- package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
- package/dist/types-CS8FIX7L.d.mts.map +0 -1
- package/dist/types-i36XcA_X.d.mts.map +0 -1
- package/dist/validate-CxVsLehf.mjs.map +0 -1
- package/dist/validate-DHxmpFJt.d.mts.map +0 -1
- package/dist/version-Bbq8TCrz.mjs +0 -7
package/src/menus/index.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Navigation menu runtime functions
|
|
2
|
+
* Navigation menu runtime functions.
|
|
3
3
|
*
|
|
4
|
-
* These are called from templates to query menus and resolve URLs.
|
|
4
|
+
* These are called from templates to query menus and resolve URLs. All queries
|
|
5
|
+
* are locale-aware: when a locale is configured (or passed explicitly) items
|
|
6
|
+
* are filtered to that locale, and menu item references resolve against the
|
|
7
|
+
* referenced content's translation_group so the URL points at the right
|
|
8
|
+
* per-locale row.
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
import type { Kysely } from "kysely";
|
|
@@ -9,50 +13,61 @@ import { sql } from "kysely";
|
|
|
9
13
|
|
|
10
14
|
import type { Database } from "../database/types.js";
|
|
11
15
|
import { validateIdentifier } from "../database/validate.js";
|
|
16
|
+
import { resolveLocale, resolveLocaleChain } from "../i18n/resolve.js";
|
|
12
17
|
import { getDb } from "../loader.js";
|
|
13
18
|
import { requestCached } from "../request-cache.js";
|
|
14
19
|
import { sanitizeHref } from "../utils/url.js";
|
|
15
20
|
import type { Menu, MenuItem, MenuItemRow } from "./types.js";
|
|
16
21
|
|
|
22
|
+
export interface MenuQueryOptions {
|
|
23
|
+
/** Override the locale used for the lookup. When omitted, the locale comes
|
|
24
|
+
* from the request context or the configured defaultLocale. */
|
|
25
|
+
locale?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
/**
|
|
18
|
-
* Get menu by name with resolved URLs
|
|
29
|
+
* Get a menu by name with resolved URLs.
|
|
19
30
|
*
|
|
20
31
|
* @example
|
|
21
32
|
* ```ts
|
|
22
|
-
* import { getMenu } from "emdash";
|
|
23
|
-
*
|
|
24
33
|
* const menu = await getMenu("primary");
|
|
25
|
-
*
|
|
26
|
-
* console.log(menu.items); // Array of MenuItem with resolved URLs
|
|
27
|
-
* }
|
|
34
|
+
* const menuEs = await getMenu("primary", { locale: "es" });
|
|
28
35
|
* ```
|
|
29
36
|
*/
|
|
30
|
-
export function getMenu(name: string): Promise<Menu | null> {
|
|
31
|
-
|
|
37
|
+
export function getMenu(name: string, options: MenuQueryOptions = {}): Promise<Menu | null> {
|
|
38
|
+
const locale = resolveLocale(options.locale);
|
|
39
|
+
return requestCached(`menu:${name}:${locale ?? "*"}`, async () => {
|
|
32
40
|
const db = await getDb();
|
|
33
|
-
return getMenuWithDb(name, db);
|
|
41
|
+
return getMenuWithDb(name, db, { locale });
|
|
34
42
|
});
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/**
|
|
38
|
-
* Get menu by name with resolved URLs (with explicit db)
|
|
39
|
-
*
|
|
40
|
-
* @internal Use `getMenu()` in templates. This variant is for admin routes
|
|
41
|
-
* that already have a database handle.
|
|
46
|
+
* Get menu by name with resolved URLs (with explicit db). Internal helper for
|
|
47
|
+
* admin routes that already have a database handle.
|
|
42
48
|
*/
|
|
43
|
-
export async function getMenuWithDb(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
export async function getMenuWithDb(
|
|
50
|
+
name: string,
|
|
51
|
+
db: Kysely<Database>,
|
|
52
|
+
options: MenuQueryOptions = {},
|
|
53
|
+
): Promise<Menu | null> {
|
|
54
|
+
const chain = resolveLocaleChain(options.locale);
|
|
55
|
+
|
|
56
|
+
const selectMenu = () => db.selectFrom("_emdash_menus").selectAll().where("name", "=", name);
|
|
57
|
+
|
|
58
|
+
let menuRow: Awaited<ReturnType<ReturnType<typeof selectMenu>["executeTakeFirst"]>>;
|
|
59
|
+
if (chain.length === 0) {
|
|
60
|
+
menuRow = await selectMenu().orderBy("locale", "asc").executeTakeFirst();
|
|
61
|
+
} else {
|
|
62
|
+
menuRow = undefined;
|
|
63
|
+
for (const locale of chain) {
|
|
64
|
+
menuRow = await selectMenu().where("locale", "=", locale).executeTakeFirst();
|
|
65
|
+
if (menuRow) break;
|
|
66
|
+
}
|
|
53
67
|
}
|
|
54
68
|
|
|
55
|
-
|
|
69
|
+
if (!menuRow) return null;
|
|
70
|
+
|
|
56
71
|
const itemRows = await db
|
|
57
72
|
.selectFrom("_emdash_menu_items")
|
|
58
73
|
.selectAll()
|
|
@@ -61,31 +76,27 @@ export async function getMenuWithDb(name: string, db: Kysely<Database>): Promise
|
|
|
61
76
|
.orderBy("sort_order", "asc")
|
|
62
77
|
.execute();
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
const items = await buildMenuTree(itemRows, db);
|
|
79
|
+
const items = await buildMenuTree(itemRows, db, menuRow.locale);
|
|
66
80
|
|
|
67
81
|
return {
|
|
68
82
|
id: menuRow.id,
|
|
69
83
|
name: menuRow.name,
|
|
70
84
|
label: menuRow.label,
|
|
71
85
|
items,
|
|
86
|
+
locale: menuRow.locale,
|
|
87
|
+
translationGroup: menuRow.translation_group,
|
|
72
88
|
};
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
/**
|
|
76
|
-
* Get all menus (without items - for admin list
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* ```ts
|
|
80
|
-
* import { getMenus } from "emdash";
|
|
81
|
-
*
|
|
82
|
-
* const menus = await getMenus();
|
|
83
|
-
* console.log(menus); // [{ id, name, label }]
|
|
84
|
-
* ```
|
|
92
|
+
* Get all menus (without items, locale-filtered — for admin list / site nav
|
|
93
|
+
* summaries). When no locale is configured, returns menus across all locales.
|
|
85
94
|
*/
|
|
86
|
-
export async function getMenus(
|
|
95
|
+
export async function getMenus(
|
|
96
|
+
options: MenuQueryOptions = {},
|
|
97
|
+
): Promise<Array<{ id: string; name: string; label: string; locale: string }>> {
|
|
87
98
|
const db = await getDb();
|
|
88
|
-
return getMenusWithDb(db);
|
|
99
|
+
return getMenusWithDb(db, options);
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
/**
|
|
@@ -96,26 +107,30 @@ export async function getMenus(): Promise<Array<{ id: string; name: string; labe
|
|
|
96
107
|
*/
|
|
97
108
|
export async function getMenusWithDb(
|
|
98
109
|
db: Kysely<Database>,
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
options: MenuQueryOptions = {},
|
|
111
|
+
): Promise<Array<{ id: string; name: string; label: string; locale: string }>> {
|
|
112
|
+
const locale = resolveLocale(options.locale);
|
|
113
|
+
let query = db
|
|
101
114
|
.selectFrom("_emdash_menus")
|
|
102
|
-
.select(["id", "name", "label"])
|
|
103
|
-
.orderBy("name", "asc")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return rows;
|
|
115
|
+
.select(["id", "name", "label", "locale"])
|
|
116
|
+
.orderBy("name", "asc");
|
|
117
|
+
if (locale !== undefined) query = query.where("locale", "=", locale);
|
|
118
|
+
return query.execute();
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
/**
|
|
110
|
-
* Build hierarchical menu tree from flat
|
|
122
|
+
* Build a hierarchical menu tree from a flat list of items. Items are
|
|
123
|
+
* resolved against the given `locale` so references land on the right
|
|
124
|
+
* per-locale content rows.
|
|
111
125
|
*/
|
|
112
|
-
async function buildMenuTree(
|
|
113
|
-
|
|
126
|
+
async function buildMenuTree(
|
|
127
|
+
items: MenuItemRow[],
|
|
128
|
+
db: Kysely<Database>,
|
|
129
|
+
locale: string,
|
|
130
|
+
): Promise<MenuItem[]> {
|
|
114
131
|
const collectionSlugs = new Set<string>();
|
|
115
132
|
for (const item of items) {
|
|
116
|
-
if (item.reference_collection)
|
|
117
|
-
collectionSlugs.add(item.reference_collection);
|
|
118
|
-
}
|
|
133
|
+
if (item.reference_collection) collectionSlugs.add(item.reference_collection);
|
|
119
134
|
if (item.type === "page" || item.type === "post") {
|
|
120
135
|
collectionSlugs.add(item.reference_collection || `${item.type}s`);
|
|
121
136
|
}
|
|
@@ -128,41 +143,28 @@ async function buildMenuTree(items: MenuItemRow[], db: Kysely<Database>): Promis
|
|
|
128
143
|
.select(["slug", "url_pattern"])
|
|
129
144
|
.where("slug", "in", [...collectionSlugs])
|
|
130
145
|
.execute();
|
|
131
|
-
for (const row of rows)
|
|
132
|
-
urlPatterns.set(row.slug, row.url_pattern);
|
|
133
|
-
}
|
|
146
|
+
for (const row of rows) urlPatterns.set(row.slug, row.url_pattern);
|
|
134
147
|
}
|
|
135
148
|
|
|
136
|
-
// Resolve all URLs first
|
|
137
149
|
const resolvedItems = await Promise.all(
|
|
138
|
-
items.map((item) => resolveMenuItem(item, db, urlPatterns)),
|
|
150
|
+
items.map((item) => resolveMenuItem(item, db, urlPatterns, locale)),
|
|
139
151
|
);
|
|
152
|
+
const validItems = resolvedItems.filter((item): item is MenuItem => item !== null);
|
|
140
153
|
|
|
141
|
-
// Filter out items that couldn't be resolved (e.g., deleted content)
|
|
142
|
-
const validItems = resolvedItems.filter((item) => item !== null);
|
|
143
|
-
|
|
144
|
-
// Build tree structure
|
|
145
154
|
const itemMap = new Map<string, MenuItem & { children: MenuItem[] }>();
|
|
146
155
|
const rootItems: MenuItem[] = [];
|
|
147
156
|
|
|
148
|
-
// First pass: create all items
|
|
149
157
|
for (const item of validItems) {
|
|
150
158
|
itemMap.set(item.id, { ...item, children: [] });
|
|
151
159
|
}
|
|
152
160
|
|
|
153
|
-
// Second pass: build parent-child relationships
|
|
154
161
|
for (const item of items) {
|
|
155
162
|
const menuItem = itemMap.get(item.id);
|
|
156
163
|
if (!menuItem) continue;
|
|
157
|
-
|
|
158
164
|
if (item.parent_id) {
|
|
159
165
|
const parent = itemMap.get(item.parent_id);
|
|
160
|
-
if (parent)
|
|
161
|
-
|
|
162
|
-
} else {
|
|
163
|
-
// Parent not found, treat as root
|
|
164
|
-
rootItems.push(menuItem);
|
|
165
|
-
}
|
|
166
|
+
if (parent) parent.children.push(menuItem);
|
|
167
|
+
else rootItems.push(menuItem);
|
|
166
168
|
} else {
|
|
167
169
|
rootItems.push(menuItem);
|
|
168
170
|
}
|
|
@@ -172,14 +174,15 @@ async function buildMenuTree(items: MenuItemRow[], db: Kysely<Database>): Promis
|
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
/**
|
|
175
|
-
* Resolve a single menu item's URL
|
|
176
|
-
*
|
|
177
|
-
*
|
|
177
|
+
* Resolve a single menu item's URL. `reference_id` is a translation_group
|
|
178
|
+
* (migration 036 remapped all existing references); we join it against
|
|
179
|
+
* the per-locale ec_* row or per-locale taxonomy row.
|
|
178
180
|
*/
|
|
179
181
|
async function resolveMenuItem(
|
|
180
182
|
item: MenuItemRow,
|
|
181
183
|
db: Kysely<Database>,
|
|
182
184
|
urlPatterns: Map<string, string | null>,
|
|
185
|
+
locale: string,
|
|
183
186
|
): Promise<MenuItem | null> {
|
|
184
187
|
let url: string | null;
|
|
185
188
|
|
|
@@ -192,24 +195,18 @@ async function resolveMenuItem(
|
|
|
192
195
|
case "page":
|
|
193
196
|
case "post":
|
|
194
197
|
url = await resolveContentUrl(
|
|
195
|
-
// Default to plural collection name (pages/posts) if not specified
|
|
196
198
|
item.reference_collection || `${item.type}s`,
|
|
197
199
|
item.reference_id,
|
|
198
200
|
db,
|
|
199
201
|
urlPatterns,
|
|
202
|
+
locale,
|
|
200
203
|
);
|
|
201
|
-
|
|
202
|
-
if (url === null) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
204
|
+
if (url === null) return null;
|
|
205
205
|
break;
|
|
206
206
|
|
|
207
207
|
case "taxonomy":
|
|
208
|
-
url = await resolveTaxonomyUrl(item.reference_id, db);
|
|
209
|
-
|
|
210
|
-
if (url === null) {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
208
|
+
url = await resolveTaxonomyUrl(item.reference_id, db, locale);
|
|
209
|
+
if (url === null) return null;
|
|
213
210
|
break;
|
|
214
211
|
|
|
215
212
|
case "collection":
|
|
@@ -223,16 +220,14 @@ async function resolveMenuItem(
|
|
|
223
220
|
item.reference_id,
|
|
224
221
|
db,
|
|
225
222
|
urlPatterns,
|
|
223
|
+
locale,
|
|
226
224
|
);
|
|
227
|
-
if (url === null)
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
225
|
+
if (url === null) return null;
|
|
230
226
|
} else {
|
|
231
227
|
url = "#";
|
|
232
228
|
}
|
|
233
229
|
}
|
|
234
230
|
} catch (error) {
|
|
235
|
-
// If resolution fails, skip this item
|
|
236
231
|
console.error(`Failed to resolve menu item ${item.id}:`, error);
|
|
237
232
|
return null;
|
|
238
233
|
}
|
|
@@ -244,7 +239,7 @@ async function resolveMenuItem(
|
|
|
244
239
|
target: item.target || undefined,
|
|
245
240
|
titleAttr: item.title_attr || undefined,
|
|
246
241
|
cssClasses: item.css_classes || undefined,
|
|
247
|
-
children: [],
|
|
242
|
+
children: [],
|
|
248
243
|
};
|
|
249
244
|
}
|
|
250
245
|
|
|
@@ -261,72 +256,96 @@ function interpolateUrlPattern(pattern: string, slug: string, id: string): strin
|
|
|
261
256
|
}
|
|
262
257
|
|
|
263
258
|
/**
|
|
264
|
-
* Resolve URL for a content
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
259
|
+
* Resolve the URL for a content reference. `referenceGroup` is the content
|
|
260
|
+
* row's translation_group; we look up the row in the requested locale
|
|
261
|
+
* (falling back to the source if no translation exists so the menu link is
|
|
262
|
+
* still clickable).
|
|
268
263
|
*/
|
|
269
264
|
async function resolveContentUrl(
|
|
270
265
|
collection: string,
|
|
271
|
-
|
|
266
|
+
referenceGroup: string | null,
|
|
272
267
|
db: Kysely<Database>,
|
|
273
268
|
urlPatterns: Map<string, string | null>,
|
|
269
|
+
locale: string,
|
|
274
270
|
): Promise<string | null> {
|
|
275
|
-
if (!
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
271
|
+
if (!referenceGroup) return null;
|
|
278
272
|
|
|
279
273
|
try {
|
|
280
|
-
// Validate collection name before interpolating into table reference
|
|
281
274
|
validateIdentifier(collection, "menu item collection");
|
|
282
275
|
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
SELECT slug FROM ${sql.ref(`ec_${collection}`)}
|
|
276
|
+
// Try the requested locale first, then any locale (deterministic).
|
|
277
|
+
let result = await sql<{ id: string; slug: string }>`
|
|
278
|
+
SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
|
|
279
|
+
WHERE translation_group = ${referenceGroup} AND locale = ${locale}
|
|
280
|
+
LIMIT 1
|
|
286
281
|
`.execute(db);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
282
|
+
let row = result.rows[0];
|
|
283
|
+
if (!row) {
|
|
284
|
+
result = await sql<{ id: string; slug: string }>`
|
|
285
|
+
SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
|
|
286
|
+
WHERE translation_group = ${referenceGroup}
|
|
287
|
+
ORDER BY locale ASC LIMIT 1
|
|
288
|
+
`.execute(db);
|
|
289
|
+
row = result.rows[0];
|
|
295
290
|
}
|
|
291
|
+
if (!row) {
|
|
292
|
+
// Legacy rows whose reference_id still points at an id directly
|
|
293
|
+
// (defensive — migration 036 normalised these, but a row inserted
|
|
294
|
+
// between migrations could predate the remap).
|
|
295
|
+
const legacy = await sql<{ id: string; slug: string }>`
|
|
296
|
+
SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
|
|
297
|
+
WHERE id = ${referenceGroup} LIMIT 1
|
|
298
|
+
`.execute(db);
|
|
299
|
+
row = legacy.rows[0];
|
|
300
|
+
}
|
|
301
|
+
if (!row) return null;
|
|
296
302
|
|
|
297
|
-
|
|
298
|
-
return
|
|
303
|
+
const pattern = urlPatterns.get(collection);
|
|
304
|
+
if (pattern) return interpolateUrlPattern(pattern, row.slug, row.id);
|
|
305
|
+
return `/${collection}/${row.slug}`;
|
|
299
306
|
} catch (error) {
|
|
300
|
-
|
|
301
|
-
console.error(`Failed to resolve content URL for ${collection}/${entryId}:`, error);
|
|
307
|
+
console.error(`Failed to resolve content URL for ${collection}/${referenceGroup}:`, error);
|
|
302
308
|
return null;
|
|
303
309
|
}
|
|
304
310
|
}
|
|
305
311
|
|
|
306
312
|
/**
|
|
307
|
-
* Resolve URL for a taxonomy term
|
|
308
|
-
*
|
|
309
|
-
* Returns null if taxonomy not found (item should be skipped)
|
|
313
|
+
* Resolve URL for a taxonomy term reference. `referenceGroup` is the term's
|
|
314
|
+
* translation_group; we pick the row in the active locale (or fall back).
|
|
310
315
|
*/
|
|
311
316
|
async function resolveTaxonomyUrl(
|
|
312
|
-
|
|
317
|
+
referenceGroup: string | null,
|
|
313
318
|
db: Kysely<Database>,
|
|
319
|
+
locale: string,
|
|
314
320
|
): Promise<string | null> {
|
|
315
|
-
if (!
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
321
|
+
if (!referenceGroup) return null;
|
|
318
322
|
|
|
319
|
-
|
|
323
|
+
let taxonomy = await db
|
|
320
324
|
.selectFrom("taxonomies")
|
|
321
325
|
.select(["name", "slug"])
|
|
322
|
-
.where("
|
|
326
|
+
.where("translation_group", "=", referenceGroup)
|
|
327
|
+
.where("locale", "=", locale)
|
|
323
328
|
.executeTakeFirst();
|
|
324
329
|
|
|
325
330
|
if (!taxonomy) {
|
|
326
|
-
|
|
327
|
-
|
|
331
|
+
taxonomy = await db
|
|
332
|
+
.selectFrom("taxonomies")
|
|
333
|
+
.select(["name", "slug"])
|
|
334
|
+
.where("translation_group", "=", referenceGroup)
|
|
335
|
+
.orderBy("locale", "asc")
|
|
336
|
+
.executeTakeFirst();
|
|
328
337
|
}
|
|
329
338
|
|
|
330
|
-
|
|
339
|
+
if (!taxonomy) {
|
|
340
|
+
// Legacy: id-based reference that predates the migration remap.
|
|
341
|
+
taxonomy = await db
|
|
342
|
+
.selectFrom("taxonomies")
|
|
343
|
+
.select(["name", "slug"])
|
|
344
|
+
.where("id", "=", referenceGroup)
|
|
345
|
+
.executeTakeFirst();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!taxonomy) return null;
|
|
349
|
+
|
|
331
350
|
return `/${taxonomy.name}/${taxonomy.slug}`;
|
|
332
351
|
}
|
package/src/menus/types.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface Menu {
|
|
|
24
24
|
name: string;
|
|
25
25
|
label: string;
|
|
26
26
|
items: MenuItem[];
|
|
27
|
+
locale: string;
|
|
28
|
+
translationGroup: string | null;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -36,13 +38,15 @@ export interface MenuItemRow {
|
|
|
36
38
|
sort_order: number;
|
|
37
39
|
type: MenuItemType;
|
|
38
40
|
reference_collection: string | null;
|
|
39
|
-
reference_id: string | null;
|
|
41
|
+
reference_id: string | null; // translation_group of referenced content/term
|
|
40
42
|
custom_url: string | null;
|
|
41
43
|
label: string;
|
|
42
44
|
title_attr: string | null;
|
|
43
45
|
target: string | null;
|
|
44
46
|
css_classes: string | null;
|
|
45
47
|
created_at: string;
|
|
48
|
+
locale: string;
|
|
49
|
+
translation_group: string | null;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/**
|
|
@@ -54,6 +58,8 @@ export interface MenuRow {
|
|
|
54
58
|
label: string;
|
|
55
59
|
created_at: string;
|
|
56
60
|
updated_at: string;
|
|
61
|
+
locale: string;
|
|
62
|
+
translation_group: string | null;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
/**
|
|
@@ -62,6 +68,11 @@ export interface MenuRow {
|
|
|
62
68
|
export interface CreateMenuItemInput {
|
|
63
69
|
type: MenuItemType;
|
|
64
70
|
label: string;
|
|
71
|
+
/**
|
|
72
|
+
* Identifier of the referenced entity. For `reference_collection` items it is
|
|
73
|
+
* the content's translation_group (locale-agnostic); for `taxonomy` items it
|
|
74
|
+
* is the term's translation_group.
|
|
75
|
+
*/
|
|
65
76
|
referenceCollection?: string;
|
|
66
77
|
referenceId?: string;
|
|
67
78
|
customUrl?: string;
|
|
@@ -91,6 +102,9 @@ export interface UpdateMenuItemInput {
|
|
|
91
102
|
export interface CreateMenuInput {
|
|
92
103
|
name: string;
|
|
93
104
|
label: string;
|
|
105
|
+
locale?: string;
|
|
106
|
+
/** When set, links the new menu into an existing translation_group. */
|
|
107
|
+
translationOf?: string;
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
/**
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site identity head injection.
|
|
3
|
+
*
|
|
4
|
+
* Emits first-party `<head>` tags sourced from the user-configured Site
|
|
5
|
+
* Identity. These are rendered alongside, but separate from, the plugin
|
|
6
|
+
* contribution pipeline (`page/metadata.ts`) because:
|
|
7
|
+
*
|
|
8
|
+
* - Site identity is first-party, not plugin-supplied. The contribution
|
|
9
|
+
* pipeline's `isSafeHref` allowlist rejects same-origin paths like
|
|
10
|
+
* `/_emdash/api/media/file/...` (which is correct for sandboxed plugin
|
|
11
|
+
* contributions, but blocks our own favicon URLs).
|
|
12
|
+
* - The data shape is fixed and small. Routing it through a generic
|
|
13
|
+
* deduper buys nothing.
|
|
14
|
+
*
|
|
15
|
+
* Currently emits only `<link rel="icon">`. Other site-identity tags
|
|
16
|
+
* (`apple-touch-icon`, `theme-color`, `application-name`) need their own
|
|
17
|
+
* configurable fields in `SiteSettings` before they ship; emitting them
|
|
18
|
+
* automatically from the favicon would produce broken icons on iOS for
|
|
19
|
+
* SVG favicons or blurry home-screen icons when the favicon is a small
|
|
20
|
+
* PNG. Tracked separately.
|
|
21
|
+
*
|
|
22
|
+
* Templates that previously emitted their own `<link rel="icon">` are
|
|
23
|
+
* getting their lines dropped in the same change that introduced this
|
|
24
|
+
* helper.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { MediaReference } from "../settings/types.js";
|
|
28
|
+
import { escapeHtmlAttr } from "./metadata.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Subset of site settings consumed by `renderSiteIdentity`. Kept narrow
|
|
32
|
+
* so callers don't have to fetch fields they don't use.
|
|
33
|
+
*/
|
|
34
|
+
export interface SiteIdentityInput {
|
|
35
|
+
favicon?: MediaReference;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build the `<head>` HTML for site identity tags. Returns an empty string
|
|
40
|
+
* when no identity fields are configured.
|
|
41
|
+
*/
|
|
42
|
+
export function renderSiteIdentity(input: SiteIdentityInput | undefined): string {
|
|
43
|
+
if (!input) return "";
|
|
44
|
+
|
|
45
|
+
const parts: string[] = [];
|
|
46
|
+
|
|
47
|
+
const favicon = input.favicon;
|
|
48
|
+
if (favicon?.url) {
|
|
49
|
+
let tag = `<link rel="icon" href="${escapeHtmlAttr(favicon.url)}"`;
|
|
50
|
+
if (favicon.contentType) {
|
|
51
|
+
tag += ` type="${escapeHtmlAttr(favicon.contentType)}"`;
|
|
52
|
+
}
|
|
53
|
+
tag += ">";
|
|
54
|
+
parts.push(tag);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return parts.join("\n");
|
|
58
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { PluginDescriptor } from "../astro/integration/runtime.js";
|
|
14
14
|
import { PLUGIN_CAPABILITIES, HOOK_NAMES } from "./manifest-schema.js";
|
|
15
|
+
import { normalizeCapabilities } from "./types.js";
|
|
15
16
|
import type {
|
|
16
17
|
StandardPluginDefinition,
|
|
17
18
|
StandardHookEntry,
|
|
@@ -147,7 +148,10 @@ export function adaptSandboxEntry(
|
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
// Build capabilities from descriptor.
|
|
150
|
-
// Validate against the known set (same as defineNativePlugin).
|
|
151
|
+
// Validate against the known set (same as defineNativePlugin). Both
|
|
152
|
+
// current and deprecated names are accepted; deprecated names are
|
|
153
|
+
// silently normalized to current names below so the runtime only ever
|
|
154
|
+
// sees the canonical form.
|
|
151
155
|
const rawCapabilities = descriptor.capabilities ?? [];
|
|
152
156
|
for (const cap of rawCapabilities) {
|
|
153
157
|
if (!VALID_CAPABILITIES_SET.has(cap)) {
|
|
@@ -157,20 +161,28 @@ export function adaptSandboxEntry(
|
|
|
157
161
|
);
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
|
|
165
|
+
// Silent normalization: rewrite deprecated names to current names.
|
|
166
|
+
// Safe assertion — `normalizeCapabilities` only emits validated input
|
|
167
|
+
// plus current names from the rename map, all of which are in the union.
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- validated above; normalizeCapabilities only returns capabilities from the union
|
|
169
|
+
const capabilities = normalizeCapabilities(rawCapabilities) as PluginCapability[];
|
|
162
170
|
const allowedHosts = descriptor.allowedHosts ?? [];
|
|
163
171
|
|
|
164
172
|
// Capability implications: broader capabilities imply narrower ones
|
|
165
|
-
// (mirrors the normalization in define-plugin.ts for native format)
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
// (mirrors the normalization in define-plugin.ts for native format).
|
|
174
|
+
// Operates on canonical names only.
|
|
175
|
+
if (capabilities.includes("content:write") && !capabilities.includes("content:read")) {
|
|
176
|
+
capabilities.push("content:read");
|
|
168
177
|
}
|
|
169
|
-
if (capabilities.includes("write
|
|
170
|
-
capabilities.push("read
|
|
178
|
+
if (capabilities.includes("media:write") && !capabilities.includes("media:read")) {
|
|
179
|
+
capabilities.push("media:read");
|
|
171
180
|
}
|
|
172
|
-
if (
|
|
173
|
-
capabilities.
|
|
181
|
+
if (
|
|
182
|
+
capabilities.includes("network:request:unrestricted") &&
|
|
183
|
+
!capabilities.includes("network:request")
|
|
184
|
+
) {
|
|
185
|
+
capabilities.push("network:request");
|
|
174
186
|
}
|
|
175
187
|
|
|
176
188
|
// Build storage config from descriptor.
|
package/src/plugins/context.ts
CHANGED
|
@@ -647,14 +647,14 @@ export function createUnrestrictedHttpAccess(pluginId: string): HttpAccess {
|
|
|
647
647
|
}
|
|
648
648
|
|
|
649
649
|
/**
|
|
650
|
-
* Create blocked HTTP access (for plugins without network:
|
|
650
|
+
* Create blocked HTTP access (for plugins without network:request capability)
|
|
651
651
|
*/
|
|
652
652
|
export function createBlockedHttpAccess(pluginId: string): HttpAccess {
|
|
653
653
|
return {
|
|
654
654
|
async fetch(): Promise<never> {
|
|
655
655
|
throw new Error(
|
|
656
|
-
`Plugin "${pluginId}" does not have the "network:
|
|
657
|
-
`Add "network:
|
|
656
|
+
`Plugin "${pluginId}" does not have the "network:request" capability. ` +
|
|
657
|
+
`Add "network:request" to the plugin's capabilities to enable HTTP requests.`,
|
|
658
658
|
);
|
|
659
659
|
},
|
|
660
660
|
};
|
|
@@ -902,32 +902,35 @@ export class PluginContextFactory {
|
|
|
902
902
|
const storage = createStorageAccess(this.db, plugin.id, plugin.storage);
|
|
903
903
|
|
|
904
904
|
// Capability-gated: content
|
|
905
|
+
// Note: capabilities reach this point already normalized to the
|
|
906
|
+
// canonical names by definePlugin / adaptSandboxEntry. Deprecated
|
|
907
|
+
// names ("read:content", "write:content") never appear here.
|
|
905
908
|
let content: ContentAccess | ContentAccessWithWrite | undefined;
|
|
906
|
-
if (capabilities.has("write
|
|
909
|
+
if (capabilities.has("content:write")) {
|
|
907
910
|
content = createContentAccessWithWrite(this.db);
|
|
908
|
-
} else if (capabilities.has("read
|
|
911
|
+
} else if (capabilities.has("content:read")) {
|
|
909
912
|
content = createContentAccess(this.db);
|
|
910
913
|
}
|
|
911
914
|
|
|
912
915
|
// Capability-gated: media
|
|
913
916
|
let media: MediaAccess | MediaAccessWithWrite | undefined;
|
|
914
|
-
if (capabilities.has("write
|
|
917
|
+
if (capabilities.has("media:write") && this.getUploadUrl) {
|
|
915
918
|
media = createMediaAccessWithWrite(this.db, this.getUploadUrl, this.storage);
|
|
916
|
-
} else if (capabilities.has("read
|
|
919
|
+
} else if (capabilities.has("media:read")) {
|
|
917
920
|
media = createMediaAccess(this.db);
|
|
918
921
|
}
|
|
919
922
|
|
|
920
923
|
// Capability-gated: http
|
|
921
924
|
let http: HttpAccess | undefined;
|
|
922
|
-
if (capabilities.has("network:
|
|
925
|
+
if (capabilities.has("network:request:unrestricted")) {
|
|
923
926
|
http = createUnrestrictedHttpAccess(plugin.id);
|
|
924
|
-
} else if (capabilities.has("network:
|
|
927
|
+
} else if (capabilities.has("network:request")) {
|
|
925
928
|
http = createHttpAccess(plugin.id, plugin.allowedHosts);
|
|
926
929
|
}
|
|
927
930
|
|
|
928
931
|
// Capability-gated: users
|
|
929
932
|
let users: UserAccess | undefined;
|
|
930
|
-
if (capabilities.has("read
|
|
933
|
+
if (capabilities.has("users:read")) {
|
|
931
934
|
users = createUserAccess(this.db);
|
|
932
935
|
}
|
|
933
936
|
|