@wildo-ai/saas-website 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +34 -0
- package/dist/esm/.builder.pid +9 -0
- package/dist/esm/astro/blog-post-bridge.d.ts +324 -0
- package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -0
- package/dist/esm/astro/blog-post-bridge.js +57 -0
- package/dist/esm/astro/blog-post-bridge.js.map +1 -0
- package/dist/esm/astro/blog-post-head.renderer.d.ts +129 -0
- package/dist/esm/astro/blog-post-head.renderer.d.ts.map +1 -0
- package/dist/esm/astro/blog-post-head.renderer.js +139 -0
- package/dist/esm/astro/blog-post-head.renderer.js.map +1 -0
- package/dist/esm/astro/bridge-runtime.d.ts +224 -0
- package/dist/esm/astro/bridge-runtime.d.ts.map +1 -0
- package/dist/esm/astro/bridge-runtime.js +30 -0
- package/dist/esm/astro/bridge-runtime.js.map +1 -0
- package/dist/esm/astro/collect-expected-label-keys.d.ts +65 -0
- package/dist/esm/astro/collect-expected-label-keys.d.ts.map +1 -0
- package/dist/esm/astro/collect-expected-label-keys.js +101 -0
- package/dist/esm/astro/collect-expected-label-keys.js.map +1 -0
- package/dist/esm/astro/headers-renderer.d.ts +32 -0
- package/dist/esm/astro/headers-renderer.d.ts.map +1 -0
- package/dist/esm/astro/headers-renderer.js +64 -0
- package/dist/esm/astro/headers-renderer.js.map +1 -0
- package/dist/esm/astro/i18n-routing.helper.d.ts +91 -0
- package/dist/esm/astro/i18n-routing.helper.d.ts.map +1 -0
- package/dist/esm/astro/i18n-routing.helper.js +23 -0
- package/dist/esm/astro/i18n-routing.helper.js.map +1 -0
- package/dist/esm/astro/internal/html-escape.d.ts +31 -0
- package/dist/esm/astro/internal/html-escape.d.ts.map +1 -0
- package/dist/esm/astro/internal/html-escape.js +40 -0
- package/dist/esm/astro/internal/html-escape.js.map +1 -0
- package/dist/esm/astro/label-pack-loader.d.ts +134 -0
- package/dist/esm/astro/label-pack-loader.d.ts.map +1 -0
- package/dist/esm/astro/label-pack-loader.js +298 -0
- package/dist/esm/astro/label-pack-loader.js.map +1 -0
- package/dist/esm/astro/llms-txt-server.d.ts +28 -0
- package/dist/esm/astro/llms-txt-server.d.ts.map +1 -0
- package/dist/esm/astro/llms-txt-server.js +33 -0
- package/dist/esm/astro/llms-txt-server.js.map +1 -0
- package/dist/esm/astro/pick-labels-for-page.d.ts +63 -0
- package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -0
- package/dist/esm/astro/pick-labels-for-page.js +73 -0
- package/dist/esm/astro/pick-labels-for-page.js.map +1 -0
- package/dist/esm/astro/robots-renderer.d.ts +85 -0
- package/dist/esm/astro/robots-renderer.d.ts.map +1 -0
- package/dist/esm/astro/robots-renderer.js +157 -0
- package/dist/esm/astro/robots-renderer.js.map +1 -0
- package/dist/esm/astro/sitemap-coverage.d.ts +101 -0
- package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -0
- package/dist/esm/astro/sitemap-coverage.js +67 -0
- package/dist/esm/astro/sitemap-coverage.js.map +1 -0
- package/dist/esm/astro/structured-data.renderer.d.ts +21 -0
- package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -0
- package/dist/esm/astro/structured-data.renderer.js +142 -0
- package/dist/esm/astro/structured-data.renderer.js.map +1 -0
- package/dist/esm/astro/website-page-head.renderer.d.ts +93 -0
- package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -0
- package/dist/esm/astro/website-page-head.renderer.js +184 -0
- package/dist/esm/astro/website-page-head.renderer.js.map +1 -0
- package/dist/esm/astro/website-page-runtime.helper.d.ts +112 -0
- package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -0
- package/dist/esm/astro/website-page-runtime.helper.js +60 -0
- package/dist/esm/astro/website-page-runtime.helper.js.map +1 -0
- package/dist/esm/astro/website-site-context.d.ts +126 -0
- package/dist/esm/astro/website-site-context.d.ts.map +1 -0
- package/dist/esm/astro/website-site-context.js +37 -0
- package/dist/esm/astro/website-site-context.js.map +1 -0
- package/dist/esm/astro-island.d.ts +56 -0
- package/dist/esm/astro-island.d.ts.map +1 -0
- package/dist/esm/astro-island.js +61 -0
- package/dist/esm/astro-island.js.map +1 -0
- package/dist/esm/astro.d.ts +151 -0
- package/dist/esm/astro.d.ts.map +1 -0
- package/dist/esm/astro.js +151 -0
- package/dist/esm/astro.js.map +1 -0
- package/dist/esm/companion-exports.d.ts +38 -0
- package/dist/esm/companion-exports.d.ts.map +1 -0
- package/dist/esm/companion-exports.js +38 -0
- package/dist/esm/companion-exports.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteButton.d.ts +73 -0
- package/dist/esm/components/low-level/WebsiteButton.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteButton.js +68 -0
- package/dist/esm/components/low-level/WebsiteButton.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteHeading.d.ts +78 -0
- package/dist/esm/components/low-level/WebsiteHeading.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteHeading.js +41 -0
- package/dist/esm/components/low-level/WebsiteHeading.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteImage.d.ts +73 -0
- package/dist/esm/components/low-level/WebsiteImage.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteImage.js +21 -0
- package/dist/esm/components/low-level/WebsiteImage.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.d.ts +90 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.js +47 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.d.ts +99 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.js +68 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteLink.d.ts +50 -0
- package/dist/esm/components/low-level/WebsiteLink.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteLink.js +22 -0
- package/dist/esm/components/low-level/WebsiteLink.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteText.d.ts +57 -0
- package/dist/esm/components/low-level/WebsiteText.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteText.js +57 -0
- package/dist/esm/components/low-level/WebsiteText.js.map +1 -0
- package/dist/esm/config/define-website-config.d.ts +83 -0
- package/dist/esm/config/define-website-config.d.ts.map +1 -0
- package/dist/esm/config/define-website-config.js +126 -0
- package/dist/esm/config/define-website-config.js.map +1 -0
- package/dist/esm/config/index.d.ts +38 -0
- package/dist/esm/config/index.d.ts.map +1 -0
- package/dist/esm/config/index.js +38 -0
- package/dist/esm/config/index.js.map +1 -0
- package/dist/esm/config/load-website-config.d.ts +136 -0
- package/dist/esm/config/load-website-config.d.ts.map +1 -0
- package/dist/esm/config/load-website-config.js +177 -0
- package/dist/esm/config/load-website-config.js.map +1 -0
- package/dist/esm/config/wildo-website-config.schemas.d.ts +251 -0
- package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -0
- package/dist/esm/config/wildo-website-config.schemas.js +302 -0
- package/dist/esm/config/wildo-website-config.schemas.js.map +1 -0
- package/dist/esm/config-loader.d.ts +35 -0
- package/dist/esm/config-loader.d.ts.map +1 -0
- package/dist/esm/config-loader.js +35 -0
- package/dist/esm/config-loader.js.map +1 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +38 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.js +77 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +41 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +58 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +81 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +177 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -0
- package/dist/esm/core/contexts/WebsitePageContext.d.ts +40 -0
- package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -0
- package/dist/esm/core/contexts/WebsitePageContext.js +8 -0
- package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +162 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.js +22 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.d.ts +47 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.js +8 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -0
- package/dist/esm/core/contexts/useWebsitePage.d.ts +13 -0
- package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -0
- package/dist/esm/core/contexts/useWebsitePage.js +22 -0
- package/dist/esm/core/contexts/useWebsitePage.js.map +1 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +19 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -0
- package/dist/esm/core/contexts/useWebsiteSection.d.ts +12 -0
- package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -0
- package/dist/esm/core/contexts/useWebsiteSection.js +22 -0
- package/dist/esm/core/contexts/useWebsiteSection.js.map +1 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +17 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.js +23 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -0
- package/dist/esm/core/factories/define-website-page-manifest.d.ts +70 -0
- package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -0
- package/dist/esm/core/factories/define-website-page-manifest.js +51 -0
- package/dist/esm/core/factories/define-website-page-manifest.js.map +1 -0
- package/dist/esm/core/factories/define-website-section.d.ts +101 -0
- package/dist/esm/core/factories/define-website-section.d.ts.map +1 -0
- package/dist/esm/core/factories/define-website-section.js +71 -0
- package/dist/esm/core/factories/define-website-section.js.map +1 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts +23 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.js +25 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.js.map +1 -0
- package/dist/esm/core/hooks/useWebsiteLabel.d.ts +68 -0
- package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -0
- package/dist/esm/core/hooks/useWebsiteLabel.js +103 -0
- package/dist/esm/core/hooks/useWebsiteLabel.js.map +1 -0
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts +87 -0
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -0
- package/dist/esm/core/layouts/WebsitePageLayout.js +170 -0
- package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -0
- package/dist/esm/core/layouts/WebsiteSection.d.ts +91 -0
- package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -0
- package/dist/esm/core/layouts/WebsiteSection.js +26 -0
- package/dist/esm/core/layouts/WebsiteSection.js.map +1 -0
- package/dist/esm/core/routing/internal-routing.utils.d.ts +65 -0
- package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -0
- package/dist/esm/core/routing/internal-routing.utils.js +14 -0
- package/dist/esm/core/routing/internal-routing.utils.js.map +1 -0
- package/dist/esm/index.d.ts +129 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +129 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/mdx/BlogPost.d.ts +193 -0
- package/dist/esm/mdx/BlogPost.d.ts.map +1 -0
- package/dist/esm/mdx/BlogPost.js +161 -0
- package/dist/esm/mdx/BlogPost.js.map +1 -0
- package/dist/esm/mdx/blog-post-collection.config.d.ts +139 -0
- package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -0
- package/dist/esm/mdx/blog-post-collection.config.js +27 -0
- package/dist/esm/mdx/blog-post-collection.config.js.map +1 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.d.ts +12 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.js +13 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.js.map +1 -0
- package/dist/esm/mdx/index.d.ts +22 -0
- package/dist/esm/mdx/index.d.ts.map +1 -0
- package/dist/esm/mdx/index.js +22 -0
- package/dist/esm/mdx/index.js.map +1 -0
- package/dist/esm/mdx/website-mdx-provider.d.ts +90 -0
- package/dist/esm/mdx/website-mdx-provider.d.ts.map +1 -0
- package/dist/esm/mdx/website-mdx-provider.js +8 -0
- package/dist/esm/mdx/website-mdx-provider.js.map +1 -0
- package/dist/esm/mdx.d.ts +38 -0
- package/dist/esm/mdx.d.ts.map +1 -0
- package/dist/esm/mdx.js +38 -0
- package/dist/esm/mdx.js.map +1 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts +75 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.js +85 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts +120 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +152 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts +73 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.js +76 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.js.map +1 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts +209 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.js +218 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts +431 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +438 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/refs/page-ref.schemas.d.ts +40 -0
- package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/refs/page-ref.schemas.js +43 -0
- package/dist/esm/schemas/refs/page-ref.schemas.js.map +1 -0
- package/dist/esm/schemas/refs/section-ref.schemas.d.ts +41 -0
- package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/refs/section-ref.schemas.js +44 -0
- package/dist/esm/schemas/refs/section-ref.schemas.js.map +1 -0
- package/dist/esm/schemas/sections/website-section-category.shared.d.ts +63 -0
- package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -0
- package/dist/esm/schemas/sections/website-section-category.shared.js +64 -0
- package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts +114 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +69 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +100 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +355 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +58 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js +2 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/validators/label-pack.validator.d.ts +144 -0
- package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -0
- package/dist/esm/schemas/validators/label-pack.validator.js +100 -0
- package/dist/esm/schemas/validators/label-pack.validator.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +143 -0
- package/src/__tests__/bundle-isolation.test.ts +607 -0
- package/src/astro/__tests__/blog-post-bridge.test.tsx +288 -0
- package/src/astro/__tests__/blog-post-head.renderer.test.ts +249 -0
- package/src/astro/__tests__/bridge-runtime.test.tsx +198 -0
- package/src/astro/__tests__/collect-expected-label-keys.test.ts +69 -0
- package/src/astro/__tests__/headers-renderer.test.ts +143 -0
- package/src/astro/__tests__/i18n-routing.helper.test.ts +116 -0
- package/src/astro/__tests__/label-pack-loader.test.ts +305 -0
- package/src/astro/__tests__/llms-txt-server.test.ts +127 -0
- package/src/astro/__tests__/pick-labels-for-page.test.ts +60 -0
- package/src/astro/__tests__/robots-renderer.test.ts +228 -0
- package/src/astro/__tests__/sitemap-coverage.test.ts +230 -0
- package/src/astro/__tests__/structured-data.renderer.test.ts +172 -0
- package/src/astro/__tests__/website-page-head.renderer.test.ts +424 -0
- package/src/astro/__tests__/website-page-runtime.helper.test.ts +157 -0
- package/src/astro/__tests__/website-site-context.test.ts +121 -0
- package/src/astro/blog-post-bridge.tsx +404 -0
- package/src/astro/blog-post-head.renderer.ts +293 -0
- package/src/astro/bridge-runtime.tsx +270 -0
- package/src/astro/collect-expected-label-keys.ts +113 -0
- package/src/astro/headers-renderer.ts +82 -0
- package/src/astro/i18n-routing.helper.ts +116 -0
- package/src/astro/internal/html-escape.ts +41 -0
- package/src/astro/label-pack-loader.ts +385 -0
- package/src/astro/llms-txt-server.ts +56 -0
- package/src/astro/pick-labels-for-page.ts +85 -0
- package/src/astro/robots-renderer.ts +171 -0
- package/src/astro/sitemap-coverage.ts +186 -0
- package/src/astro/structured-data.renderer.ts +171 -0
- package/src/astro/website-page-head.renderer.ts +279 -0
- package/src/astro/website-page-runtime.helper.ts +188 -0
- package/src/astro/website-site-context.ts +136 -0
- package/src/astro-island.ts +61 -0
- package/src/astro.ts +151 -0
- package/src/companion-exports.ts +38 -0
- package/src/components/low-level/WebsiteButton.tsx +118 -0
- package/src/components/low-level/WebsiteHeading.tsx +96 -0
- package/src/components/low-level/WebsiteImage.tsx +96 -0
- package/src/components/low-level/WebsiteInternalButton.tsx +172 -0
- package/src/components/low-level/WebsiteInternalLink.tsx +193 -0
- package/src/components/low-level/WebsiteLink.tsx +71 -0
- package/src/components/low-level/WebsiteText.tsx +75 -0
- package/src/components/low-level/__tests__/WebsiteInternalButton.test.tsx +169 -0
- package/src/components/low-level/__tests__/WebsiteInternalLink.test.tsx +202 -0
- package/src/components/low-level/__tests__/primitives.test.tsx +141 -0
- package/src/config/__tests__/define-website-config.test.ts +231 -0
- package/src/config/__tests__/load-website-config.test.ts +215 -0
- package/src/config/define-website-config.ts +134 -0
- package/src/config/index.ts +38 -0
- package/src/config/load-website-config.ts +229 -0
- package/src/config/wildo-website-config.schemas.ts +322 -0
- package/src/config-loader.ts +35 -0
- package/src/core/__tests__/WebsitePageLayout.test.tsx +184 -0
- package/src/core/__tests__/WebsiteSection.test.tsx +85 -0
- package/src/core/__tests__/contexts.test.tsx +133 -0
- package/src/core/__tests__/core-boundary.test.ts +85 -0
- package/src/core/__tests__/define-website-page-manifest.test.tsx +111 -0
- package/src/core/__tests__/define-website-section.test.tsx +89 -0
- package/src/core/__tests__/useWebsiteDesignTokens.test.tsx +41 -0
- package/src/core/__tests__/useWebsiteLabel.test.tsx +117 -0
- package/src/core/anonymous-session/InboundContactForm.tsx +157 -0
- package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +44 -0
- package/src/core/anonymous-session/inbound-contact-form.schema.ts +62 -0
- package/src/core/anonymous-session/website-anonymous-session-client.ts +194 -0
- package/src/core/contexts/WebsitePageContext.tsx +52 -0
- package/src/core/contexts/WebsiteRuntimeContext.tsx +175 -0
- package/src/core/contexts/WebsiteSectionContext.tsx +59 -0
- package/src/core/contexts/useWebsitePage.ts +28 -0
- package/src/core/contexts/useWebsiteRuntime.ts +34 -0
- package/src/core/contexts/useWebsiteSection.ts +28 -0
- package/src/core/external-providers/frontend-provider-registry.website.ts +62 -0
- package/src/core/factories/define-website-page-manifest.ts +102 -0
- package/src/core/factories/define-website-section.ts +128 -0
- package/src/core/hooks/useWebsiteDesignTokens.ts +26 -0
- package/src/core/hooks/useWebsiteLabel.ts +121 -0
- package/src/core/layouts/WebsitePageLayout.tsx +311 -0
- package/src/core/layouts/WebsiteSection.tsx +139 -0
- package/src/core/routing/__tests__/internal-routing.utils.test.ts +134 -0
- package/src/core/routing/internal-routing.utils.ts +77 -0
- package/src/index.ts +132 -0
- package/src/mdx/BlogPost.tsx +296 -0
- package/src/mdx/__tests__/BlogPost.test.tsx +218 -0
- package/src/mdx/__tests__/blog-post-collection.config.test.ts +127 -0
- package/src/mdx/__tests__/website-mdx-provider.test.tsx +82 -0
- package/src/mdx/blog-post-collection.config.ts +177 -0
- package/src/mdx/blog-post-frontmatter-source.ts +31 -0
- package/src/mdx/index.ts +39 -0
- package/src/mdx/website-mdx-provider.tsx +95 -0
- package/src/mdx.ts +38 -0
- package/src/schemas/__tests__/blog-frontmatter.test.ts +50 -0
- package/src/schemas/__tests__/companion-exports-react-free.test.ts +51 -0
- package/src/schemas/__tests__/design-tokens.test.ts +163 -0
- package/src/schemas/__tests__/label-key.test.ts +46 -0
- package/src/schemas/__tests__/label-pack-validator.test.ts +146 -0
- package/src/schemas/__tests__/page-manifest.test.ts +66 -0
- package/src/schemas/__tests__/refs.test.ts +37 -0
- package/src/schemas/__tests__/root-config.test.ts +292 -0
- package/src/schemas/__tests__/section-definition.test.ts +47 -0
- package/src/schemas/__tests__/structured-data-reconciliation.test.ts +543 -0
- package/src/schemas/blog/blog-post-frontmatter.shared.schemas.ts +111 -0
- package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +166 -0
- package/src/schemas/label-keys/website-label-key.schemas.ts +88 -0
- package/src/schemas/manifests/website-page-manifest.shared.schemas.ts +266 -0
- package/src/schemas/manifests/website-root-config.shared.schemas.ts +504 -0
- package/src/schemas/refs/page-ref.schemas.ts +49 -0
- package/src/schemas/refs/section-ref.schemas.ts +50 -0
- package/src/schemas/sections/website-section-category.shared.ts +65 -0
- package/src/schemas/sections/website-section-definition.shared.schemas.ts +120 -0
- package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +579 -0
- package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +62 -0
- package/src/schemas/validators/label-pack.validator.ts +201 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils.js';
|
|
2
|
+
import { escapeHtmlAttribute, escapeHtmlText } from './internal/html-escape.js';
|
|
3
|
+
import { renderPageStructuredData } from './structured-data.renderer.js';
|
|
4
|
+
export function buildBlogPostStructuredDataPayload(opts) {
|
|
5
|
+
const { frontmatter, canonicalUrl, siteOrigin, siteName, locale } = opts;
|
|
6
|
+
return {
|
|
7
|
+
'@type': 'BlogPosting',
|
|
8
|
+
'@id': `${canonicalUrl}#blog-post`,
|
|
9
|
+
headline: frontmatter.title,
|
|
10
|
+
description: frontmatter.summary,
|
|
11
|
+
datePublished: frontmatter.publishedAt,
|
|
12
|
+
author: {
|
|
13
|
+
'@type': 'Person',
|
|
14
|
+
name: String(frontmatter.authorRef),
|
|
15
|
+
},
|
|
16
|
+
publisher: {
|
|
17
|
+
'@type': 'Organization',
|
|
18
|
+
name: siteName,
|
|
19
|
+
url: siteOrigin,
|
|
20
|
+
},
|
|
21
|
+
mainEntityOfPage: canonicalUrl,
|
|
22
|
+
url: canonicalUrl,
|
|
23
|
+
inLanguage: String(locale),
|
|
24
|
+
...(frontmatter.coverImageUrl ? { image: frontmatter.coverImageUrl } : {}),
|
|
25
|
+
...(frontmatter.tags.length > 0 ? { keywords: frontmatter.tags.join(', ') } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function renderBlogPostHeadHtml(opts) {
|
|
29
|
+
const { siteContext, locale, routePath, frontmatter, noaiOptOut } = opts;
|
|
30
|
+
const { rootConfig, websiteConfig } = siteContext;
|
|
31
|
+
const title = frontmatter.title;
|
|
32
|
+
const description = frontmatter.summary;
|
|
33
|
+
const ogImageUrl = frontmatter.coverImageUrl;
|
|
34
|
+
const titleAttr = escapeHtmlAttribute(title);
|
|
35
|
+
const descriptionAttr = escapeHtmlAttribute(description);
|
|
36
|
+
const titleText = escapeHtmlText(title);
|
|
37
|
+
const siteOrigin = websiteConfig.publicOrigin;
|
|
38
|
+
const defaultLocale = websiteConfig.defaultLocale;
|
|
39
|
+
const currentLocaleStr = String(locale);
|
|
40
|
+
/**
|
|
41
|
+
* Same locale-prefix composition rule as `renderWebsitePageHeadHtml`
|
|
42
|
+
* — delegated to the shared `buildLocalizedRoutePath` helper so a
|
|
43
|
+
* future change to the prefix policy lands in ONE place and propagates
|
|
44
|
+
* consistently to internal links + page heads + blog-post heads.
|
|
45
|
+
*/
|
|
46
|
+
function buildAbsoluteUrl(forLocale) {
|
|
47
|
+
const path = buildLocalizedRoutePath({
|
|
48
|
+
locale: forLocale,
|
|
49
|
+
defaultLocale,
|
|
50
|
+
routePath,
|
|
51
|
+
});
|
|
52
|
+
return `${siteOrigin}${path}`;
|
|
53
|
+
}
|
|
54
|
+
const canonicalUrl = buildAbsoluteUrl(locale);
|
|
55
|
+
const hreflangLines = [];
|
|
56
|
+
for (const entry of websiteConfig.locales) {
|
|
57
|
+
const localeStr = String(entry.locale);
|
|
58
|
+
hreflangLines.push(`<link rel="alternate" hreflang="${escapeHtmlAttribute(localeStr)}" href="${escapeHtmlAttribute(buildAbsoluteUrl(entry.locale))}" />`);
|
|
59
|
+
}
|
|
60
|
+
hreflangLines.push(`<link rel="alternate" hreflang="x-default" href="${escapeHtmlAttribute(buildAbsoluteUrl(defaultLocale))}" />`);
|
|
61
|
+
const ogImageLines = [];
|
|
62
|
+
if (ogImageUrl !== undefined) {
|
|
63
|
+
const ogImageAttr = escapeHtmlAttribute(ogImageUrl);
|
|
64
|
+
ogImageLines.push(`<meta property="og:image" content="${ogImageAttr}" />`, `<meta name="twitter:image" content="${ogImageAttr}" />`);
|
|
65
|
+
}
|
|
66
|
+
const twitterCardKind = ogImageUrl !== undefined ? 'summary_large_image' : 'summary';
|
|
67
|
+
const ogLocaleAlternateLines = [];
|
|
68
|
+
for (const entry of websiteConfig.locales) {
|
|
69
|
+
const localeStr = String(entry.locale);
|
|
70
|
+
if (localeStr === currentLocaleStr)
|
|
71
|
+
continue;
|
|
72
|
+
ogLocaleAlternateLines.push(`<meta property="og:locale:alternate" content="${escapeHtmlAttribute(localeStr)}" />`);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* `og:article:*` block — the per-post metadata that distinguishes
|
|
76
|
+
* the blog-post head from the page head. The Open Graph "article"
|
|
77
|
+
* vertical defines:
|
|
78
|
+
* - `article:published_time` (ISO-8601 — already validated by
|
|
79
|
+
* the frontmatter schema's `z.iso.datetime`).
|
|
80
|
+
* - `article:author` (free text or URL — we emit the author
|
|
81
|
+
* ref as a free-text string; a future author-registry
|
|
82
|
+
* resolution would emit the author's profile URL instead).
|
|
83
|
+
* - `article:tag` (one tag per emission; the OG spec uses
|
|
84
|
+
* repeated `<meta property="article:tag">` lines, NOT a
|
|
85
|
+
* comma-joined list).
|
|
86
|
+
*
|
|
87
|
+
* `article:section` (the OG "section" field, e.g. "Engineering" /
|
|
88
|
+
* "Product") is intentionally NOT emitted — Wonder Todos has no
|
|
89
|
+
* blog-section taxonomy yet, and emitting an empty section would
|
|
90
|
+
* be worse than omission.
|
|
91
|
+
*/
|
|
92
|
+
const articleMetaLines = [
|
|
93
|
+
`<meta property="article:published_time" content="${escapeHtmlAttribute(frontmatter.publishedAt)}" />`,
|
|
94
|
+
`<meta property="article:author" content="${escapeHtmlAttribute(frontmatter.authorRef)}" />`,
|
|
95
|
+
];
|
|
96
|
+
for (const tag of frontmatter.tags) {
|
|
97
|
+
articleMetaLines.push(`<meta property="article:tag" content="${escapeHtmlAttribute(tag)}" />`);
|
|
98
|
+
}
|
|
99
|
+
const siteNameAttr = escapeHtmlAttribute(rootConfig.seo.siteName);
|
|
100
|
+
const canonicalAttr = escapeHtmlAttribute(canonicalUrl);
|
|
101
|
+
const ogLocaleAttr = escapeHtmlAttribute(currentLocaleStr);
|
|
102
|
+
const noaiMetaLines = [];
|
|
103
|
+
if (noaiOptOut === true) {
|
|
104
|
+
noaiMetaLines.push(`<meta name="robots" content="noai, noimageai" />`);
|
|
105
|
+
}
|
|
106
|
+
const structuredDataHtml = renderPageStructuredData({
|
|
107
|
+
pagePayloads: [
|
|
108
|
+
buildBlogPostStructuredDataPayload({
|
|
109
|
+
frontmatter,
|
|
110
|
+
canonicalUrl,
|
|
111
|
+
siteOrigin,
|
|
112
|
+
siteName: rootConfig.seo.siteName,
|
|
113
|
+
locale,
|
|
114
|
+
}),
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
return [
|
|
118
|
+
`<title>${titleText}</title>`,
|
|
119
|
+
`<meta name="description" content="${descriptionAttr}" />`,
|
|
120
|
+
`<link rel="canonical" href="${canonicalAttr}" />`,
|
|
121
|
+
`<meta property="og:title" content="${titleAttr}" />`,
|
|
122
|
+
`<meta property="og:description" content="${descriptionAttr}" />`,
|
|
123
|
+
`<meta property="og:type" content="article" />`,
|
|
124
|
+
`<meta property="og:url" content="${canonicalAttr}" />`,
|
|
125
|
+
`<meta property="og:site_name" content="${siteNameAttr}" />`,
|
|
126
|
+
`<meta property="og:locale" content="${ogLocaleAttr}" />`,
|
|
127
|
+
...ogLocaleAlternateLines,
|
|
128
|
+
...ogImageLines,
|
|
129
|
+
`<meta name="twitter:card" content="${twitterCardKind}" />`,
|
|
130
|
+
`<meta name="twitter:title" content="${titleAttr}" />`,
|
|
131
|
+
`<meta name="twitter:description" content="${descriptionAttr}" />`,
|
|
132
|
+
...articleMetaLines,
|
|
133
|
+
...noaiMetaLines,
|
|
134
|
+
structuredDataHtml,
|
|
135
|
+
...hreflangLines,
|
|
136
|
+
].join('\n');
|
|
137
|
+
}
|
|
138
|
+
/** @wildo_source:part:end saas.website.blog.head-renderer */
|
|
139
|
+
//# sourceMappingURL=blog-post-head.renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-post-head.renderer.js","sourceRoot":"","sources":["../../../../src/astro/blog-post-head.renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils';\nimport type { BlogPostFrontmatter } from '../schemas/blog/blog-post-frontmatter.shared.schemas';\nimport type { WebsiteStructuredDataPayload } from '../schemas/structured-data/website-structured-data.shared.schemas';\n\nimport { escapeHtmlAttribute, escapeHtmlText } from './internal/html-escape';\nimport { renderPageStructuredData } from './structured-data.renderer';\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.blog.head-renderer facet:layer:astro facet:family:website\n *\n * `renderBlogPostHeadHtml(opts)` — sibling of `renderWebsitePageHeadHtml`\n * that emits the SEO `<head>` block for an individual blog post,\n * sourcing per-render copy from a validated `BlogPostFrontmatter`\n * instead of the page manifest's `metaLabelKeys`.\n *\n * # Why a separate renderer (not an overload of the page-head renderer)\n *\n * The framework-mindset rule \"every type / bucket / field must carry\n * its semantic weight\" pushes against overloading\n * `renderWebsitePageHeadHtml` with a \"blog-post mode\" flag:\n *\n * - The two renderers have STRUCTURALLY different inputs.\n * `renderWebsitePageHeadHtml` reads `manifest.metaLabelKeys.{titleKey,\n * descriptionKey, ogImageUrl}` + the matching `labelPack` lookup;\n * this renderer reads `frontmatter.{title, summary, coverImageUrl,\n * publishedAt, authorRef, tags}` directly — no label-pack\n * dependency at all (blog body copy is per-post, not\n * per-locale-pack, by design).\n * - The two renderers emit DIFFERENT meta surfaces.\n * `og:type=website` vs. `og:type=article`; the article variant\n * additionally emits `article:published_time`,\n * `article:author`, and `article:tag` (one per `frontmatter.tags`\n * entry). Folding both into one helper would require an\n * `articleMeta?: { ... }` discriminated branch that carries no\n * value when called from a non-blog page — exactly the kind of\n * \"convenience union\" the framework rules forbid.\n * - The blog-post route itself uses a parameterized `routePath`\n * (`/blog/[slug]`) that needs the SLUG substituted at render time\n * to produce the absolute canonical/hreflang URLs. Page-level\n * routes are always literal — keeping the substitution in this\n * renderer (rather than at the manifest layer) means the page\n * manifest stays free of per-render concerns.\n *\n * Both renderers share the exact same hreflang/canonical/OG locale\n * composition logic — duplicating ~30 lines of stable URL composition\n * is the lesser evil compared to threading a generic options bag\n * through both. If a third renderer ever lands (a \"campaign-page\" or\n * \"tag-page\" head emitter, say), the right move is to extract a\n * shared `composeHreflangAndCanonical(...)` helper at that point;\n * doing it now would be premature against the YAGNI line.\n *\n * # The `routePath` argument\n *\n * The blog-post page manifest's `routePath` is `/blog/[slug]` (Astro\n * route-parameter syntax). At render time the consumer's `[slug].astro`\n * has already resolved the slug, so we accept the FULLY-SUBSTITUTED\n * locale-neutral path (e.g. `/blog/why-context-beats-content`) here.\n * `buildLocalizedRoutePath` then handles the locale-prefix decision,\n * mirroring how `WebsiteInternalLink` and `renderWebsitePageHeadHtml`\n * handle their own routes — the locale-prefix rule never lives in two\n * places.\n *\n * # Failure modes\n *\n * Throws nothing — all required frontmatter fields are guaranteed by\n * `parseBlogPostFrontmatter` upstream (same Zod 4 schema). The\n * optional `coverImageUrl` is the only branch: when absent, the\n * Twitter card kind degrades to `summary` (text-only) and `og:image`\n * is omitted entirely.\n */\n\nexport interface RenderBlogPostHeadHtmlOptions {\n /**\n * Composed `WebsiteSiteContext` — the renderer reads\n * `websiteConfig.{defaultLocale, locales, publicOrigin}` for the\n * URL/locale surface and `rootConfig.seo.{siteName}` for the\n * brand fallback. `seo.defaultOgImageUrl` is intentionally NOT\n * consulted: blog posts that ship without a `coverImageUrl` get a\n * smaller-card degradation rather than a generic site-wide image\n * (the default OG image is meaningful for marketing pages but\n * misleading for blog posts).\n */\n siteContext: WebsiteSiteContext;\n /**\n * The locale Astro resolved (with default-locale fallback) for the\n * current request. Drives the `<html lang>` value upstream + the\n * `og:locale` line and the canonical URL's locale-prefix decision.\n */\n locale: AvailableLanguage;\n /**\n * The fully-substituted locale-neutral route path for the post —\n * typically `/blog/<slug>` with `<slug>` already resolved by the\n * consumer's `[slug].astro` `getStaticPaths()`. Passing the\n * substituted path (rather than the parameterized one) keeps the\n * renderer agnostic of the consumer's route-parameter naming.\n */\n routePath: string;\n /**\n * Validated frontmatter — the result of `parseBlogPostFrontmatter`.\n * Carrying the parsed (branded) shape here means an unvalidated\n * `entry.data` cannot reach this renderer accidentally (the\n * branded-type signature would reject a raw `unknown`).\n */\n frontmatter: BlogPostFrontmatter;\n /**\n * AI-training opt-out (D-AG3) — when `true`, the renderer emits\n * `<meta name=\"robots\" content=\"noai, noimageai\">`. The sibling\n * `_headers` entry is produced by `renderBlogPostNoaiHeaders()`\n * (one knob, two channels — see the page-head renderer's\n * companion).\n *\n * **Why a renderer-level option, not a frontmatter field**: the\n * blog-post frontmatter schema (`BlogPostFrontmatterSchema`)\n * doesn't yet expose a per-post AI-opt-out knob — when it does,\n * the consumer's `[slug].astro` will read it from the parsed\n * frontmatter and pass it through here. Keeping the option at\n * the renderer surface NOW means a third-party blog-post catalog\n * (campaigns, customer stories) can opt out without authoring a\n * new frontmatter schema first.\n */\n noaiOptOut?: boolean;\n}\n\nexport interface BuildBlogPostStructuredDataPayloadOptions {\n frontmatter: BlogPostFrontmatter;\n canonicalUrl: string;\n siteOrigin: string;\n siteName: string;\n locale: AvailableLanguage;\n}\n\nexport function buildBlogPostStructuredDataPayload(\n opts: BuildBlogPostStructuredDataPayloadOptions,\n): WebsiteStructuredDataPayload {\n const { frontmatter, canonicalUrl, siteOrigin, siteName, locale } = opts;\n return {\n '@type': 'BlogPosting',\n '@id': `${canonicalUrl}#blog-post`,\n headline: frontmatter.title,\n description: frontmatter.summary,\n datePublished: frontmatter.publishedAt,\n author: {\n '@type': 'Person',\n name: String(frontmatter.authorRef),\n },\n publisher: {\n '@type': 'Organization',\n name: siteName,\n url: siteOrigin,\n },\n mainEntityOfPage: canonicalUrl,\n url: canonicalUrl,\n inLanguage: String(locale),\n ...(frontmatter.coverImageUrl ? { image: frontmatter.coverImageUrl } : {}),\n ...(frontmatter.tags.length > 0 ? { keywords: frontmatter.tags.join(', ') } : {}),\n };\n}\n\nexport function renderBlogPostHeadHtml(opts: RenderBlogPostHeadHtmlOptions): string {\n const { siteContext, locale, routePath, frontmatter, noaiOptOut } = opts;\n const { rootConfig, websiteConfig } = siteContext;\n\n const title = frontmatter.title;\n const description = frontmatter.summary;\n const ogImageUrl = frontmatter.coverImageUrl;\n const titleAttr = escapeHtmlAttribute(title);\n const descriptionAttr = escapeHtmlAttribute(description);\n const titleText = escapeHtmlText(title);\n\n const siteOrigin = websiteConfig.publicOrigin;\n const defaultLocale = websiteConfig.defaultLocale;\n const currentLocaleStr = String(locale);\n\n /**\n * Same locale-prefix composition rule as `renderWebsitePageHeadHtml`\n * — delegated to the shared `buildLocalizedRoutePath` helper so a\n * future change to the prefix policy lands in ONE place and propagates\n * consistently to internal links + page heads + blog-post heads.\n */\n function buildAbsoluteUrl(forLocale: AvailableLanguage): string {\n const path = buildLocalizedRoutePath({\n locale: forLocale,\n defaultLocale,\n routePath,\n });\n return `${siteOrigin}${path}`;\n }\n\n const canonicalUrl = buildAbsoluteUrl(locale);\n\n const hreflangLines: string[] = [];\n for (const entry of websiteConfig.locales) {\n const localeStr = String(entry.locale);\n hreflangLines.push(\n `<link rel=\"alternate\" hreflang=\"${escapeHtmlAttribute(localeStr)}\" href=\"${escapeHtmlAttribute(buildAbsoluteUrl(entry.locale))}\" />`,\n );\n }\n hreflangLines.push(\n `<link rel=\"alternate\" hreflang=\"x-default\" href=\"${escapeHtmlAttribute(buildAbsoluteUrl(defaultLocale))}\" />`,\n );\n\n const ogImageLines: string[] = [];\n if (ogImageUrl !== undefined) {\n const ogImageAttr = escapeHtmlAttribute(ogImageUrl);\n ogImageLines.push(\n `<meta property=\"og:image\" content=\"${ogImageAttr}\" />`,\n `<meta name=\"twitter:image\" content=\"${ogImageAttr}\" />`,\n );\n }\n const twitterCardKind = ogImageUrl !== undefined ? 'summary_large_image' : 'summary';\n\n const ogLocaleAlternateLines: string[] = [];\n for (const entry of websiteConfig.locales) {\n const localeStr = String(entry.locale);\n if (localeStr === currentLocaleStr) continue;\n ogLocaleAlternateLines.push(\n `<meta property=\"og:locale:alternate\" content=\"${escapeHtmlAttribute(localeStr)}\" />`,\n );\n }\n\n /**\n * `og:article:*` block — the per-post metadata that distinguishes\n * the blog-post head from the page head. The Open Graph \"article\"\n * vertical defines:\n * - `article:published_time` (ISO-8601 — already validated by\n * the frontmatter schema's `z.iso.datetime`).\n * - `article:author` (free text or URL — we emit the author\n * ref as a free-text string; a future author-registry\n * resolution would emit the author's profile URL instead).\n * - `article:tag` (one tag per emission; the OG spec uses\n * repeated `<meta property=\"article:tag\">` lines, NOT a\n * comma-joined list).\n *\n * `article:section` (the OG \"section\" field, e.g. \"Engineering\" /\n * \"Product\") is intentionally NOT emitted — Wonder Todos has no\n * blog-section taxonomy yet, and emitting an empty section would\n * be worse than omission.\n */\n const articleMetaLines: string[] = [\n `<meta property=\"article:published_time\" content=\"${escapeHtmlAttribute(frontmatter.publishedAt)}\" />`,\n `<meta property=\"article:author\" content=\"${escapeHtmlAttribute(frontmatter.authorRef as unknown as string)}\" />`,\n ];\n for (const tag of frontmatter.tags) {\n articleMetaLines.push(\n `<meta property=\"article:tag\" content=\"${escapeHtmlAttribute(tag as unknown as string)}\" />`,\n );\n }\n\n const siteNameAttr = escapeHtmlAttribute(rootConfig.seo.siteName);\n const canonicalAttr = escapeHtmlAttribute(canonicalUrl);\n const ogLocaleAttr = escapeHtmlAttribute(currentLocaleStr);\n const noaiMetaLines: string[] = [];\n if (noaiOptOut === true) {\n noaiMetaLines.push(`<meta name=\"robots\" content=\"noai, noimageai\" />`);\n }\n\n const structuredDataHtml = renderPageStructuredData({\n pagePayloads: [\n buildBlogPostStructuredDataPayload({\n frontmatter,\n canonicalUrl,\n siteOrigin,\n siteName: rootConfig.seo.siteName,\n locale,\n }),\n ],\n });\n\n return [\n `<title>${titleText}</title>`,\n `<meta name=\"description\" content=\"${descriptionAttr}\" />`,\n `<link rel=\"canonical\" href=\"${canonicalAttr}\" />`,\n `<meta property=\"og:title\" content=\"${titleAttr}\" />`,\n `<meta property=\"og:description\" content=\"${descriptionAttr}\" />`,\n `<meta property=\"og:type\" content=\"article\" />`,\n `<meta property=\"og:url\" content=\"${canonicalAttr}\" />`,\n `<meta property=\"og:site_name\" content=\"${siteNameAttr}\" />`,\n `<meta property=\"og:locale\" content=\"${ogLocaleAttr}\" />`,\n ...ogLocaleAlternateLines,\n ...ogImageLines,\n `<meta name=\"twitter:card\" content=\"${twitterCardKind}\" />`,\n `<meta name=\"twitter:title\" content=\"${titleAttr}\" />`,\n `<meta name=\"twitter:description\" content=\"${descriptionAttr}\" />`,\n ...articleMetaLines,\n ...noaiMetaLines,\n structuredDataHtml,\n ...hreflangLines,\n ].join('\\n');\n}\n/** @wildo_source:part:end saas.website.blog.head-renderer */\n"]}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { ComponentType, FunctionComponent } from 'react';
|
|
2
|
+
import type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';
|
|
3
|
+
import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
4
|
+
import type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';
|
|
5
|
+
import type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';
|
|
6
|
+
import type { WebsiteNavigationConfig } from '../schemas/manifests/website-root-config.shared.schemas';
|
|
7
|
+
import type { WebsitePageRef } from '../schemas/refs/page-ref.schemas';
|
|
8
|
+
import type { LabelPack } from '../schemas/validators/label-pack.validator';
|
|
9
|
+
/**
|
|
10
|
+
* @wildo_source:part:start saas.website.astro.bridge facet:layer:astro facet:family:website
|
|
11
|
+
*
|
|
12
|
+
* `createWebsitePageBridgeIsland(deps)` — factory that returns the
|
|
13
|
+
* React island a consumer's per-page `.astro` file mounts with a
|
|
14
|
+
* `client:idle` (or `client:load`) directive.
|
|
15
|
+
*
|
|
16
|
+
* **The hard architectural constraint this design solves**: Astro
|
|
17
|
+
* serializes React-island props via `devalue`-style JSON to transfer
|
|
18
|
+
* them across the SSR → client-hydration boundary. Function values —
|
|
19
|
+
* including `ComponentType` references — are NOT serializable. Any
|
|
20
|
+
* design that tried to pass `pageComponent`, `headerComponent`, or
|
|
21
|
+
* `footerComponent` AS island props would render correctly server-side
|
|
22
|
+
* (the function ref is in-process during SSR) and then mismatch on
|
|
23
|
+
* first hydration (the client receives `undefined`/broken placeholders
|
|
24
|
+
* for the function-typed fields). The Vitest suite would not catch
|
|
25
|
+
* this because Vitest renders React in a single process — there is no
|
|
26
|
+
* serialization boundary in unit tests.
|
|
27
|
+
*
|
|
28
|
+
* **The factory pattern fixes the constraint by closing over** the
|
|
29
|
+
* `pageComponent`, `pageRef`, and `navigation` (header / footer
|
|
30
|
+
* component refs) at MODULE LOAD TIME on the consumer side. The
|
|
31
|
+
* returned island accepts only PRIMITIVE-VALUED props (`locale`,
|
|
32
|
+
* `labelPack`, `designTokens`) — all JSON-serializable, all safe to
|
|
33
|
+
* cross the hydration boundary. The component refs never appear in
|
|
34
|
+
* the prop signature; they live in the lexical closure that the
|
|
35
|
+
* client-side bundle includes naturally as part of the island's
|
|
36
|
+
* compiled module.
|
|
37
|
+
*
|
|
38
|
+
* **Authoring pattern (per-page bridge module)**:
|
|
39
|
+
*
|
|
40
|
+
* ```tsx
|
|
41
|
+
* // consumer's src/pages-impl/landing.bridge.tsx
|
|
42
|
+
* import { createWebsitePageBridgeIsland } from '@wildo-ai/saas-website/astro-island';
|
|
43
|
+
* import { LandingPage, LANDING_PAGE_MANIFEST } from './landing.page';
|
|
44
|
+
* import { WEBSITE_ROOT_CONFIG } from '../website-root.config';
|
|
45
|
+
*
|
|
46
|
+
* export const LandingBridgeIsland = createWebsitePageBridgeIsland({
|
|
47
|
+
* PageComponent: LandingPage,
|
|
48
|
+
* pageRef: LANDING_PAGE_MANIFEST.ref,
|
|
49
|
+
* navigation: WEBSITE_ROOT_CONFIG.navigation,
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* ```astro
|
|
54
|
+
* ---
|
|
55
|
+
* // consumer's src/pages/index.astro
|
|
56
|
+
* import {
|
|
57
|
+
* loadWebsitePageRuntime,
|
|
58
|
+
* renderWebsitePageHeadHtml,
|
|
59
|
+
* renderWebsiteDesignTokensCss,
|
|
60
|
+
* } from '@wildo-ai/saas-website/astro';
|
|
61
|
+
* import { LandingBridgeIsland } from '../pages-impl/landing.bridge';
|
|
62
|
+
* import { LANDING_PAGE_MANIFEST } from '../pages-impl/landing.page';
|
|
63
|
+
* import { WEBSITE_ROOT_CONFIG } from '../website-root.config';
|
|
64
|
+
*
|
|
65
|
+
* const runtime = loadWebsitePageRuntime({
|
|
66
|
+
* manifest: LANDING_PAGE_MANIFEST,
|
|
67
|
+
* rootConfig: WEBSITE_ROOT_CONFIG,
|
|
68
|
+
* labelPacksDir: './src/i18n',
|
|
69
|
+
* astroLocale: Astro.currentLocale,
|
|
70
|
+
* });
|
|
71
|
+
* const headHtml = renderWebsitePageHeadHtml({
|
|
72
|
+
* manifest: LANDING_PAGE_MANIFEST,
|
|
73
|
+
* rootConfig: WEBSITE_ROOT_CONFIG,
|
|
74
|
+
* locale: runtime.locale,
|
|
75
|
+
* labelPack: runtime.labelPack,
|
|
76
|
+
* });
|
|
77
|
+
* const designTokensCss = renderWebsiteDesignTokensCss(
|
|
78
|
+
* WEBSITE_ROOT_CONFIG.designTokens,
|
|
79
|
+
* );
|
|
80
|
+
* ---
|
|
81
|
+
* <!doctype html>
|
|
82
|
+
* <html lang={runtime.locale}>
|
|
83
|
+
* <head>
|
|
84
|
+
* <Fragment set:html={headHtml} />
|
|
85
|
+
* <style set:html={designTokensCss}></style>
|
|
86
|
+
* </head>
|
|
87
|
+
* <body>
|
|
88
|
+
* <LandingBridgeIsland
|
|
89
|
+
* client:idle
|
|
90
|
+
* locale={runtime.locale}
|
|
91
|
+
* labelPack={runtime.labelPack}
|
|
92
|
+
* designTokens={WEBSITE_ROOT_CONFIG.designTokens}
|
|
93
|
+
* />
|
|
94
|
+
* </body>
|
|
95
|
+
* </html>
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* **Why a per-page bridge file** (rather than constructing the island
|
|
99
|
+
* inline in the consumer's `.astro` frontmatter):
|
|
100
|
+
*
|
|
101
|
+
* Astro's `client:*` directive requires the JSX tag to reference a
|
|
102
|
+
* statically-resolvable module export at compile time — that is how
|
|
103
|
+
* Astro knows which module to bundle for client-side hydration. A
|
|
104
|
+
* factory call inside `.astro` frontmatter (`const Island =
|
|
105
|
+
* createWebsitePageBridgeIsland({...}); <Island client:idle/>`) does
|
|
106
|
+
* NOT round-trip through Astro's module-graph analyzer: Astro would
|
|
107
|
+
* see a runtime-constructed component reference and fail to bundle
|
|
108
|
+
* the closure for the client. The per-page bridge file gives Astro a
|
|
109
|
+
* static export to bind the directive to. The cost is one ~6-line
|
|
110
|
+
* file per page; the win is the only architecture that is safe
|
|
111
|
+
* against the H-2 hydration-mismatch failure mode AND uses Astro's
|
|
112
|
+
* native bundling pipeline.
|
|
113
|
+
*
|
|
114
|
+
* **Why `ComponentType` refs in `Deps` and not in `Props`**: this is
|
|
115
|
+
* the entire point of the redesign. `Deps` is consumed at module load
|
|
116
|
+
* time on the consumer side — the closure traps the function refs
|
|
117
|
+
* inside the returned component. `Props` is consumed at island-mount
|
|
118
|
+
* time and crosses Astro's serialization boundary. Keeping the two
|
|
119
|
+
* concerns lexically separate makes the boundary visible in the type
|
|
120
|
+
* signature.
|
|
121
|
+
*
|
|
122
|
+
* **Boundary** (D12 client-bundle discipline): this module ships into
|
|
123
|
+
* the React-island bundle. It MUST stay clear of `node:*` imports,
|
|
124
|
+
* fs / path helpers, and the SSR-tier specifiers
|
|
125
|
+
* (`axios`, `jsonwebtoken`, …). The bundle-isolation test catches
|
|
126
|
+
* regressions both globally (closure-wide allow-list) AND with the
|
|
127
|
+
* targeted Phase-5 negative test that asserts `node:fs` / `node:path`
|
|
128
|
+
* reach this island closure ONLY through `astro.ts`, never through
|
|
129
|
+
* `index.ts` or `companion-exports.ts`.
|
|
130
|
+
*/
|
|
131
|
+
/**
|
|
132
|
+
* Module-load-time dependencies — closed over inside the returned
|
|
133
|
+
* island. NEVER cross the hydration boundary as serialized props.
|
|
134
|
+
*/
|
|
135
|
+
export interface CreateWebsitePageBridgeIslandDeps {
|
|
136
|
+
/**
|
|
137
|
+
* The page's React component. Typically the same value that was
|
|
138
|
+
* passed to `defineWebsitePageManifest({ pageComponent })` — the
|
|
139
|
+
* authoring convention re-imports it here to keep the closure
|
|
140
|
+
* explicit and the per-page bridge file self-contained.
|
|
141
|
+
*
|
|
142
|
+
* Typed as `ComponentType<any>` to mirror
|
|
143
|
+
* `WebsitePageManifest.pageComponent` and dodge React's `ComponentType`
|
|
144
|
+
* contravariance: a narrower `ComponentType<TProps>` returned by
|
|
145
|
+
* `defineWebsitePageManifest<TProps>` would not be assignable to a
|
|
146
|
+
* `ComponentType<unknown>` slot, blocking every per-page bridge
|
|
147
|
+
* call site. The component is invoked with no props at the
|
|
148
|
+
* `<PageComponent />` JSX call site below, so widening here is
|
|
149
|
+
* runtime-safe.
|
|
150
|
+
*/
|
|
151
|
+
PageComponent: ComponentType<any>;
|
|
152
|
+
/**
|
|
153
|
+
* The active page's ref. Carried as a primitive (string-branded)
|
|
154
|
+
* value so the inner `WebsitePageLayout` can register the
|
|
155
|
+
* `WebsitePageContext` without re-resolving it from the manifest.
|
|
156
|
+
*/
|
|
157
|
+
pageRef: WebsitePageRef;
|
|
158
|
+
/**
|
|
159
|
+
* Site-wide navigation slot bindings (header / footer components).
|
|
160
|
+
* Closed over here rather than passed as island props because both
|
|
161
|
+
* fields are `ComponentType` refs that cannot survive Astro's
|
|
162
|
+
* serialization boundary.
|
|
163
|
+
*/
|
|
164
|
+
navigation: WebsiteNavigationConfig;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Hydration-boundary props — what the consumer's `.astro` page passes
|
|
168
|
+
* to `<LandingBridgeIsland client:idle ...{primitiveProps}/>`. Every
|
|
169
|
+
* field is JSON-serializable.
|
|
170
|
+
*/
|
|
171
|
+
export interface WebsitePageBridgeIslandProps {
|
|
172
|
+
/** The locale Astro resolved (with default-locale fallback). */
|
|
173
|
+
locale: AvailableLanguage;
|
|
174
|
+
/**
|
|
175
|
+
* The site's default locale. Carried as an island prop (not closed
|
|
176
|
+
* over in `Deps`) because it is JSON-primitive and consumers
|
|
177
|
+
* already pass `runtime.locale` from the per-locale Astro page
|
|
178
|
+
* frontmatter — passing the matching `runtime.defaultLocale` from
|
|
179
|
+
* the same `loadWebsitePageRuntime(...)` return value keeps the
|
|
180
|
+
* authoring story symmetric. Required for `WebsiteInternalLink`
|
|
181
|
+
* (Phase 6 fix H-β) which needs both locales to compute the
|
|
182
|
+
* correct prefix.
|
|
183
|
+
*/
|
|
184
|
+
defaultLocale: AvailableLanguage;
|
|
185
|
+
/** Already-loaded + already-shape-validated label pack. */
|
|
186
|
+
labelPack: LabelPack;
|
|
187
|
+
/** Design-token bundle from `WebsiteRootConfig.designTokens`. */
|
|
188
|
+
designTokens: WebsiteDesignTokens;
|
|
189
|
+
/**
|
|
190
|
+
* Fully-composed label key the framework reads to render the
|
|
191
|
+
* "Skip to main content" affordance (Phase 6 third audit fix M-α).
|
|
192
|
+
* Mirrors `WebsiteRootConfig.skipToMainContentLabelKey`. Carried as
|
|
193
|
+
* a JSON-serializable string prop (NOT closed over in `Deps`)
|
|
194
|
+
* because it is a primitive value the consumer already has on the
|
|
195
|
+
* root config in the `.astro` frontmatter — passing it as an island
|
|
196
|
+
* prop keeps the per-page bridge module dependency-free of the
|
|
197
|
+
* site root config (the bridge stays page-local; only the per-page
|
|
198
|
+
* `.astro` page knows about `WEBSITE_ROOT_CONFIG`).
|
|
199
|
+
*/
|
|
200
|
+
skipToMainContentLabelKey: WebsiteLabelKey;
|
|
201
|
+
/** Optional website-facing public providers materialized at build time. */
|
|
202
|
+
frontendProviders?: FrontendProvidersBlock;
|
|
203
|
+
/**
|
|
204
|
+
* Optional backend API origin (no trailing slash) forwarded into
|
|
205
|
+
* `WebsiteRuntimeContext.apiOrigin` for anonymous-capture sections
|
|
206
|
+
* (lead / draft-note forms) to construct their
|
|
207
|
+
* `WebsiteAnonymousSessionClient`. A JSON-serializable string, so it
|
|
208
|
+
* crosses the hydration boundary as an island prop; the `.astro` page
|
|
209
|
+
* resolves the per-environment value from
|
|
210
|
+
* `WildoMarketingWebsiteConfig.apiOrigin`. Omitted on pages with no
|
|
211
|
+
* capture section.
|
|
212
|
+
*/
|
|
213
|
+
apiOrigin?: string;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function
|
|
217
|
+
* component — Astro hydrates nothing else here — and the union is not callable, so the wider
|
|
218
|
+
* declaration cost callers the ability to invoke the island directly. Mirrors
|
|
219
|
+
* `createWebsiteBlogPostBridgeIsland`, whose suite depends on that call. `FunctionComponent` is
|
|
220
|
+
* assignable to `ComponentType`, so no consumer narrows.
|
|
221
|
+
*/
|
|
222
|
+
export declare function createWebsitePageBridgeIsland(deps: CreateWebsitePageBridgeIslandDeps): FunctionComponent<WebsitePageBridgeIslandProps>;
|
|
223
|
+
/** @wildo_source:part:end saas.website.astro.bridge */
|
|
224
|
+
//# sourceMappingURL=bridge-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-runtime.d.ts","sourceRoot":"","sources":["../../../../src/astro/bridge-runtime.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAa,MAAM,OAAO,CAAC;AAEzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAQ9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+DAA+D,CAAC;AACzG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iDAAiD,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yDAAyD,CAAC;AACvG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyHG;AAEH;;;GAGG;AACH,MAAM,WAAW,iCAAiC;IAChD;;;;;;;;;;;;;;OAcG;IACH,aAAa,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC;IAClC;;;;OAIG;IACH,OAAO,EAAE,cAAc,CAAC;IACxB;;;;;OAKG;IACH,UAAU,EAAE,uBAAuB,CAAC;CACrC;AAED;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC3C,gEAAgE;IAChE,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;;;;;;OASG;IACH,aAAa,EAAE,iBAAiB,CAAC;IACjC,2DAA2D;IAC3D,SAAS,EAAE,SAAS,CAAC;IACrB,iEAAiE;IACjE,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;;;;;;;OAUG;IACH,yBAAyB,EAAE,eAAe,CAAC;IAC3C,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,sBAAsB,CAAC;IAC3C;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,iCAAiC,GACtC,iBAAiB,CAAC,4BAA4B,CAAC,CAiCjD;AACD,uDAAuD"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { WebsiteRuntimeContextProvider, } from '../core/contexts/WebsiteRuntimeContext.js';
|
|
3
|
+
import { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website.js';
|
|
4
|
+
import { WebsitePageLayout } from '../core/layouts/WebsitePageLayout.js';
|
|
5
|
+
/**
|
|
6
|
+
* Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function
|
|
7
|
+
* component — Astro hydrates nothing else here — and the union is not callable, so the wider
|
|
8
|
+
* declaration cost callers the ability to invoke the island directly. Mirrors
|
|
9
|
+
* `createWebsiteBlogPostBridgeIsland`, whose suite depends on that call. `FunctionComponent` is
|
|
10
|
+
* assignable to `ComponentType`, so no consumer narrows.
|
|
11
|
+
*/
|
|
12
|
+
export function createWebsitePageBridgeIsland(deps) {
|
|
13
|
+
const { PageComponent, pageRef, navigation } = deps;
|
|
14
|
+
function WebsitePageBridgeIsland({ locale, defaultLocale, labelPack, designTokens, skipToMainContentLabelKey, frontendProviders, apiOrigin, }) {
|
|
15
|
+
const runtimeValue = {
|
|
16
|
+
locale,
|
|
17
|
+
defaultLocale,
|
|
18
|
+
labelPack,
|
|
19
|
+
designTokens,
|
|
20
|
+
navigation,
|
|
21
|
+
skipToMainContentLabelKey,
|
|
22
|
+
frontendProviderRegistry: createFrontendWebsiteProviderRegistry(frontendProviders),
|
|
23
|
+
apiOrigin,
|
|
24
|
+
};
|
|
25
|
+
return (_jsx(WebsiteRuntimeContextProvider, { value: runtimeValue, children: _jsx(WebsitePageLayout, { pageRef: pageRef, children: _jsx(PageComponent, {}) }) }));
|
|
26
|
+
}
|
|
27
|
+
WebsitePageBridgeIsland.displayName = 'WebsitePageBridgeIsland';
|
|
28
|
+
return WebsitePageBridgeIsland;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=bridge-runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-runtime.js","sourceRoot":"","sources":["../../../../src/astro/bridge-runtime.tsx"],"names":[],"mappings":";AAKA,OAAO,EACL,6BAA6B,GAE9B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,qCAAqC,EAAE,MAAM,+DAA+D,CAAC;AACtH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAwNtE;;;;;;GAMG;AACH,MAAM,UAAU,6BAA6B,CAC3C,IAAuC;IAEvC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAEpD,SAAS,uBAAuB,CAAC,EAC/B,MAAM,EACN,aAAa,EACb,SAAS,EACT,YAAY,EACZ,yBAAyB,EACzB,iBAAiB,EACjB,SAAS,GACoB;QAC7B,MAAM,YAAY,GAA+B;YAC/C,MAAM;YACN,aAAa;YACb,SAAS;YACT,YAAY;YACZ,UAAU;YACV,yBAAyB;YACzB,wBAAwB,EAAE,qCAAqC,CAAC,iBAAiB,CAAC;YAClF,SAAS;SACV,CAAC;QACF,OAAO,CACL,KAAC,6BAA6B,IAAC,KAAK,EAAE,YAAY,YAChD,KAAC,iBAAiB,IAAC,OAAO,EAAE,OAAO,YACjC,KAAC,aAAa,KAAG,GACC,GACU,CACjC,CAAC;IACJ,CAAC;IACD,uBAAuB,CAAC,WAAW,GAAG,yBAAyB,CAAC;IAEhE,OAAO,uBAAuB,CAAC;AACjC,CAAC","sourcesContent":["import type { ComponentType, FunctionComponent, ReactNode } from 'react';\n\nimport type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';\nimport type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport {\n WebsiteRuntimeContextProvider,\n type WebsiteRuntimeContextValue,\n} from '../core/contexts/WebsiteRuntimeContext';\nimport { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website';\nimport { WebsitePageLayout } from '../core/layouts/WebsitePageLayout';\nimport type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';\nimport type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';\nimport type { WebsiteNavigationConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport type { WebsitePageRef } from '../schemas/refs/page-ref.schemas';\nimport type { LabelPack } from '../schemas/validators/label-pack.validator';\n\n/**\n * @wildo_source:part:start saas.website.astro.bridge facet:layer:astro facet:family:website\n *\n * `createWebsitePageBridgeIsland(deps)` — factory that returns the\n * React island a consumer's per-page `.astro` file mounts with a\n * `client:idle` (or `client:load`) directive.\n *\n * **The hard architectural constraint this design solves**: Astro\n * serializes React-island props via `devalue`-style JSON to transfer\n * them across the SSR → client-hydration boundary. Function values —\n * including `ComponentType` references — are NOT serializable. Any\n * design that tried to pass `pageComponent`, `headerComponent`, or\n * `footerComponent` AS island props would render correctly server-side\n * (the function ref is in-process during SSR) and then mismatch on\n * first hydration (the client receives `undefined`/broken placeholders\n * for the function-typed fields). The Vitest suite would not catch\n * this because Vitest renders React in a single process — there is no\n * serialization boundary in unit tests.\n *\n * **The factory pattern fixes the constraint by closing over** the\n * `pageComponent`, `pageRef`, and `navigation` (header / footer\n * component refs) at MODULE LOAD TIME on the consumer side. The\n * returned island accepts only PRIMITIVE-VALUED props (`locale`,\n * `labelPack`, `designTokens`) — all JSON-serializable, all safe to\n * cross the hydration boundary. The component refs never appear in\n * the prop signature; they live in the lexical closure that the\n * client-side bundle includes naturally as part of the island's\n * compiled module.\n *\n * **Authoring pattern (per-page bridge module)**:\n *\n * ```tsx\n * // consumer's src/pages-impl/landing.bridge.tsx\n * import { createWebsitePageBridgeIsland } from '@wildo-ai/saas-website/astro-island';\n * import { LandingPage, LANDING_PAGE_MANIFEST } from './landing.page';\n * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';\n *\n * export const LandingBridgeIsland = createWebsitePageBridgeIsland({\n * PageComponent: LandingPage,\n * pageRef: LANDING_PAGE_MANIFEST.ref,\n * navigation: WEBSITE_ROOT_CONFIG.navigation,\n * });\n * ```\n *\n * ```astro\n * ---\n * // consumer's src/pages/index.astro\n * import {\n * loadWebsitePageRuntime,\n * renderWebsitePageHeadHtml,\n * renderWebsiteDesignTokensCss,\n * } from '@wildo-ai/saas-website/astro';\n * import { LandingBridgeIsland } from '../pages-impl/landing.bridge';\n * import { LANDING_PAGE_MANIFEST } from '../pages-impl/landing.page';\n * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';\n *\n * const runtime = loadWebsitePageRuntime({\n * manifest: LANDING_PAGE_MANIFEST,\n * rootConfig: WEBSITE_ROOT_CONFIG,\n * labelPacksDir: './src/i18n',\n * astroLocale: Astro.currentLocale,\n * });\n * const headHtml = renderWebsitePageHeadHtml({\n * manifest: LANDING_PAGE_MANIFEST,\n * rootConfig: WEBSITE_ROOT_CONFIG,\n * locale: runtime.locale,\n * labelPack: runtime.labelPack,\n * });\n * const designTokensCss = renderWebsiteDesignTokensCss(\n * WEBSITE_ROOT_CONFIG.designTokens,\n * );\n * ---\n * <!doctype html>\n * <html lang={runtime.locale}>\n * <head>\n * <Fragment set:html={headHtml} />\n * <style set:html={designTokensCss}></style>\n * </head>\n * <body>\n * <LandingBridgeIsland\n * client:idle\n * locale={runtime.locale}\n * labelPack={runtime.labelPack}\n * designTokens={WEBSITE_ROOT_CONFIG.designTokens}\n * />\n * </body>\n * </html>\n * ```\n *\n * **Why a per-page bridge file** (rather than constructing the island\n * inline in the consumer's `.astro` frontmatter):\n *\n * Astro's `client:*` directive requires the JSX tag to reference a\n * statically-resolvable module export at compile time — that is how\n * Astro knows which module to bundle for client-side hydration. A\n * factory call inside `.astro` frontmatter (`const Island =\n * createWebsitePageBridgeIsland({...}); <Island client:idle/>`) does\n * NOT round-trip through Astro's module-graph analyzer: Astro would\n * see a runtime-constructed component reference and fail to bundle\n * the closure for the client. The per-page bridge file gives Astro a\n * static export to bind the directive to. The cost is one ~6-line\n * file per page; the win is the only architecture that is safe\n * against the H-2 hydration-mismatch failure mode AND uses Astro's\n * native bundling pipeline.\n *\n * **Why `ComponentType` refs in `Deps` and not in `Props`**: this is\n * the entire point of the redesign. `Deps` is consumed at module load\n * time on the consumer side — the closure traps the function refs\n * inside the returned component. `Props` is consumed at island-mount\n * time and crosses Astro's serialization boundary. Keeping the two\n * concerns lexically separate makes the boundary visible in the type\n * signature.\n *\n * **Boundary** (D12 client-bundle discipline): this module ships into\n * the React-island bundle. It MUST stay clear of `node:*` imports,\n * fs / path helpers, and the SSR-tier specifiers\n * (`axios`, `jsonwebtoken`, …). The bundle-isolation test catches\n * regressions both globally (closure-wide allow-list) AND with the\n * targeted Phase-5 negative test that asserts `node:fs` / `node:path`\n * reach this island closure ONLY through `astro.ts`, never through\n * `index.ts` or `companion-exports.ts`.\n */\n\n/**\n * Module-load-time dependencies — closed over inside the returned\n * island. NEVER cross the hydration boundary as serialized props.\n */\nexport interface CreateWebsitePageBridgeIslandDeps {\n /**\n * The page's React component. Typically the same value that was\n * passed to `defineWebsitePageManifest({ pageComponent })` — the\n * authoring convention re-imports it here to keep the closure\n * explicit and the per-page bridge file self-contained.\n *\n * Typed as `ComponentType<any>` to mirror\n * `WebsitePageManifest.pageComponent` and dodge React's `ComponentType`\n * contravariance: a narrower `ComponentType<TProps>` returned by\n * `defineWebsitePageManifest<TProps>` would not be assignable to a\n * `ComponentType<unknown>` slot, blocking every per-page bridge\n * call site. The component is invoked with no props at the\n * `<PageComponent />` JSX call site below, so widening here is\n * runtime-safe.\n */\n PageComponent: ComponentType<any>;\n /**\n * The active page's ref. Carried as a primitive (string-branded)\n * value so the inner `WebsitePageLayout` can register the\n * `WebsitePageContext` without re-resolving it from the manifest.\n */\n pageRef: WebsitePageRef;\n /**\n * Site-wide navigation slot bindings (header / footer components).\n * Closed over here rather than passed as island props because both\n * fields are `ComponentType` refs that cannot survive Astro's\n * serialization boundary.\n */\n navigation: WebsiteNavigationConfig;\n}\n\n/**\n * Hydration-boundary props — what the consumer's `.astro` page passes\n * to `<LandingBridgeIsland client:idle ...{primitiveProps}/>`. Every\n * field is JSON-serializable.\n */\nexport interface WebsitePageBridgeIslandProps {\n /** The locale Astro resolved (with default-locale fallback). */\n locale: AvailableLanguage;\n /**\n * The site's default locale. Carried as an island prop (not closed\n * over in `Deps`) because it is JSON-primitive and consumers\n * already pass `runtime.locale` from the per-locale Astro page\n * frontmatter — passing the matching `runtime.defaultLocale` from\n * the same `loadWebsitePageRuntime(...)` return value keeps the\n * authoring story symmetric. Required for `WebsiteInternalLink`\n * (Phase 6 fix H-β) which needs both locales to compute the\n * correct prefix.\n */\n defaultLocale: AvailableLanguage;\n /** Already-loaded + already-shape-validated label pack. */\n labelPack: LabelPack;\n /** Design-token bundle from `WebsiteRootConfig.designTokens`. */\n designTokens: WebsiteDesignTokens;\n /**\n * Fully-composed label key the framework reads to render the\n * \"Skip to main content\" affordance (Phase 6 third audit fix M-α).\n * Mirrors `WebsiteRootConfig.skipToMainContentLabelKey`. Carried as\n * a JSON-serializable string prop (NOT closed over in `Deps`)\n * because it is a primitive value the consumer already has on the\n * root config in the `.astro` frontmatter — passing it as an island\n * prop keeps the per-page bridge module dependency-free of the\n * site root config (the bridge stays page-local; only the per-page\n * `.astro` page knows about `WEBSITE_ROOT_CONFIG`).\n */\n skipToMainContentLabelKey: WebsiteLabelKey;\n /** Optional website-facing public providers materialized at build time. */\n frontendProviders?: FrontendProvidersBlock;\n /**\n * Optional backend API origin (no trailing slash) forwarded into\n * `WebsiteRuntimeContext.apiOrigin` for anonymous-capture sections\n * (lead / draft-note forms) to construct their\n * `WebsiteAnonymousSessionClient`. A JSON-serializable string, so it\n * crosses the hydration boundary as an island prop; the `.astro` page\n * resolves the per-environment value from\n * `WildoMarketingWebsiteConfig.apiOrigin`. Omitted on pages with no\n * capture section.\n */\n apiOrigin?: string;\n}\n\n/**\n * Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function\n * component — Astro hydrates nothing else here — and the union is not callable, so the wider\n * declaration cost callers the ability to invoke the island directly. Mirrors\n * `createWebsiteBlogPostBridgeIsland`, whose suite depends on that call. `FunctionComponent` is\n * assignable to `ComponentType`, so no consumer narrows.\n */\nexport function createWebsitePageBridgeIsland(\n deps: CreateWebsitePageBridgeIslandDeps,\n): FunctionComponent<WebsitePageBridgeIslandProps> {\n const { PageComponent, pageRef, navigation } = deps;\n\n function WebsitePageBridgeIsland({\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n skipToMainContentLabelKey,\n frontendProviders,\n apiOrigin,\n }: WebsitePageBridgeIslandProps): ReactNode {\n const runtimeValue: WebsiteRuntimeContextValue = {\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n navigation,\n skipToMainContentLabelKey,\n frontendProviderRegistry: createFrontendWebsiteProviderRegistry(frontendProviders),\n apiOrigin,\n };\n return (\n <WebsiteRuntimeContextProvider value={runtimeValue}>\n <WebsitePageLayout pageRef={pageRef}>\n <PageComponent />\n </WebsitePageLayout>\n </WebsiteRuntimeContextProvider>\n );\n }\n WebsitePageBridgeIsland.displayName = 'WebsitePageBridgeIsland';\n\n return WebsitePageBridgeIsland;\n}\n/** @wildo_source:part:end saas.website.astro.bridge */\n"]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { WebsiteSectionDefinition } from '../schemas/sections/website-section-definition.shared.schemas';
|
|
2
|
+
import type { ExpectedLabelKeysBySectionRef } from '../schemas/validators/label-pack.validator';
|
|
3
|
+
/**
|
|
4
|
+
* @wildo_source:part:start saas.website.astro.label-key-collector facet:layer:astro facet:family:website
|
|
5
|
+
*
|
|
6
|
+
* `collectExpectedLabelKeysFromSections(sections)` — pure, no-I/O
|
|
7
|
+
* collector that materializes the
|
|
8
|
+
* `ExpectedLabelKeysBySectionRef` map the label-pack validator
|
|
9
|
+
* (`validateLabelPackAgainstManifest`) consumes.
|
|
10
|
+
*
|
|
11
|
+
* **Why an explicit-list collector and not a JSX walker**
|
|
12
|
+
* (Phase 5 plan originally proposed walking each manifest's
|
|
13
|
+
* `pageComponent` to discover its `defineWebsiteSection` calls —
|
|
14
|
+
* we deliberately rejected that approach):
|
|
15
|
+
*
|
|
16
|
+
* - **No runtime React introspection.** A JSX walker would either
|
|
17
|
+
* (a) require booting React + a virtual DOM at build time just to
|
|
18
|
+
* render every page once and observe its section composition, or
|
|
19
|
+
* (b) require static AST analysis of the `pageComponent` source
|
|
20
|
+
* file to discover `<HeroSection/>`-style imports. Both add a
|
|
21
|
+
* non-trivial dependency surface (jsdom or a TS AST walker) and
|
|
22
|
+
* both fail silently on dynamic composition (a section rendered
|
|
23
|
+
* conditionally by a feature flag is invisible to the static
|
|
24
|
+
* walker but visible to the renderer, and vice versa).
|
|
25
|
+
* - **The consumer already has the list.** Each page-impl module
|
|
26
|
+
* already imports its sections (that's how JSX renders them);
|
|
27
|
+
* `WebsiteRootConfig` already lists every page manifest; the
|
|
28
|
+
* consumer's `website-root.config.ts` is therefore one line away
|
|
29
|
+
* from also exporting a flat `[HeroSection, PricingSection, …]`
|
|
30
|
+
* array. Asking for that array trades a frame of build-time magic
|
|
31
|
+
* for a single hand-authored line and turns "did the build see
|
|
32
|
+
* this section?" into a TypeScript-checked import — a far better
|
|
33
|
+
* trade than a JSX walker.
|
|
34
|
+
* - **Section reuse across pages is naturally handled.** Two pages
|
|
35
|
+
* reusing the same `HeroSection` definition (with the same
|
|
36
|
+
* `sectionRef`) collide silently in a JSX walker (which would
|
|
37
|
+
* register the section twice with identical expected keys).
|
|
38
|
+
* Here, the array deduplicates per `sectionRef` (a same-ref
|
|
39
|
+
* duplicate with NON-matching `expectedLabelKeys` is a hard
|
|
40
|
+
* authoring bug — see refinement below).
|
|
41
|
+
*
|
|
42
|
+
* **Refinement — duplicate `sectionRef` with non-matching keys**:
|
|
43
|
+
* if the same `sectionRef` appears twice with different
|
|
44
|
+
* `expectedLabelKeys`, the collector throws. This catches the
|
|
45
|
+
* "two distinct sections accidentally using the same ref" bug
|
|
46
|
+
* (typically a copy-paste during section authoring) at build time
|
|
47
|
+
* with an actionable message rather than letting the second
|
|
48
|
+
* `Map.set` silently overwrite the first.
|
|
49
|
+
*
|
|
50
|
+
* **Pure / no I/O**: the collector is invoked at build start by
|
|
51
|
+
* the Astro adapter and at audit time by `wildo website audit`;
|
|
52
|
+
* it does not read files and does not boot React.
|
|
53
|
+
*
|
|
54
|
+
* @param sections Every `WebsiteSectionDefinition` registered on the
|
|
55
|
+
* site. Order does not matter — the result is an
|
|
56
|
+
* unordered map keyed by `sectionRef`.
|
|
57
|
+
* @returns A `Map<WebsiteSectionRef, readonly WebsiteLabelKey[]>`
|
|
58
|
+
* ready to feed into `validateLabelPackAgainstManifest`.
|
|
59
|
+
* @throws When two distinct sections share a `sectionRef` but
|
|
60
|
+
* declare different `expectedLabelKeys` arrays (length or
|
|
61
|
+
* membership) — the collector's hard-error path.
|
|
62
|
+
*/
|
|
63
|
+
export declare function collectExpectedLabelKeysFromSections(sections: readonly WebsiteSectionDefinition[]): ExpectedLabelKeysBySectionRef;
|
|
64
|
+
/** @wildo_source:part:end saas.website.astro.label-key-collector */
|
|
65
|
+
//# sourceMappingURL=collect-expected-label-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect-expected-label-keys.d.ts","sourceRoot":"","sources":["../../../../src/astro/collect-expected-label-keys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+DAA+D,CAAC;AAG9G,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,4CAA4C,CAAC;AAEhG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,wBAAgB,oCAAoC,CAClD,QAAQ,EAAE,SAAS,wBAAwB,EAAE,GAC5C,6BAA6B,CAsB/B;AAuBD,oEAAoE"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wildo_source:part:start saas.website.astro.label-key-collector facet:layer:astro facet:family:website
|
|
3
|
+
*
|
|
4
|
+
* `collectExpectedLabelKeysFromSections(sections)` — pure, no-I/O
|
|
5
|
+
* collector that materializes the
|
|
6
|
+
* `ExpectedLabelKeysBySectionRef` map the label-pack validator
|
|
7
|
+
* (`validateLabelPackAgainstManifest`) consumes.
|
|
8
|
+
*
|
|
9
|
+
* **Why an explicit-list collector and not a JSX walker**
|
|
10
|
+
* (Phase 5 plan originally proposed walking each manifest's
|
|
11
|
+
* `pageComponent` to discover its `defineWebsiteSection` calls —
|
|
12
|
+
* we deliberately rejected that approach):
|
|
13
|
+
*
|
|
14
|
+
* - **No runtime React introspection.** A JSX walker would either
|
|
15
|
+
* (a) require booting React + a virtual DOM at build time just to
|
|
16
|
+
* render every page once and observe its section composition, or
|
|
17
|
+
* (b) require static AST analysis of the `pageComponent` source
|
|
18
|
+
* file to discover `<HeroSection/>`-style imports. Both add a
|
|
19
|
+
* non-trivial dependency surface (jsdom or a TS AST walker) and
|
|
20
|
+
* both fail silently on dynamic composition (a section rendered
|
|
21
|
+
* conditionally by a feature flag is invisible to the static
|
|
22
|
+
* walker but visible to the renderer, and vice versa).
|
|
23
|
+
* - **The consumer already has the list.** Each page-impl module
|
|
24
|
+
* already imports its sections (that's how JSX renders them);
|
|
25
|
+
* `WebsiteRootConfig` already lists every page manifest; the
|
|
26
|
+
* consumer's `website-root.config.ts` is therefore one line away
|
|
27
|
+
* from also exporting a flat `[HeroSection, PricingSection, …]`
|
|
28
|
+
* array. Asking for that array trades a frame of build-time magic
|
|
29
|
+
* for a single hand-authored line and turns "did the build see
|
|
30
|
+
* this section?" into a TypeScript-checked import — a far better
|
|
31
|
+
* trade than a JSX walker.
|
|
32
|
+
* - **Section reuse across pages is naturally handled.** Two pages
|
|
33
|
+
* reusing the same `HeroSection` definition (with the same
|
|
34
|
+
* `sectionRef`) collide silently in a JSX walker (which would
|
|
35
|
+
* register the section twice with identical expected keys).
|
|
36
|
+
* Here, the array deduplicates per `sectionRef` (a same-ref
|
|
37
|
+
* duplicate with NON-matching `expectedLabelKeys` is a hard
|
|
38
|
+
* authoring bug — see refinement below).
|
|
39
|
+
*
|
|
40
|
+
* **Refinement — duplicate `sectionRef` with non-matching keys**:
|
|
41
|
+
* if the same `sectionRef` appears twice with different
|
|
42
|
+
* `expectedLabelKeys`, the collector throws. This catches the
|
|
43
|
+
* "two distinct sections accidentally using the same ref" bug
|
|
44
|
+
* (typically a copy-paste during section authoring) at build time
|
|
45
|
+
* with an actionable message rather than letting the second
|
|
46
|
+
* `Map.set` silently overwrite the first.
|
|
47
|
+
*
|
|
48
|
+
* **Pure / no I/O**: the collector is invoked at build start by
|
|
49
|
+
* the Astro adapter and at audit time by `wildo website audit`;
|
|
50
|
+
* it does not read files and does not boot React.
|
|
51
|
+
*
|
|
52
|
+
* @param sections Every `WebsiteSectionDefinition` registered on the
|
|
53
|
+
* site. Order does not matter — the result is an
|
|
54
|
+
* unordered map keyed by `sectionRef`.
|
|
55
|
+
* @returns A `Map<WebsiteSectionRef, readonly WebsiteLabelKey[]>`
|
|
56
|
+
* ready to feed into `validateLabelPackAgainstManifest`.
|
|
57
|
+
* @throws When two distinct sections share a `sectionRef` but
|
|
58
|
+
* declare different `expectedLabelKeys` arrays (length or
|
|
59
|
+
* membership) — the collector's hard-error path.
|
|
60
|
+
*/
|
|
61
|
+
export function collectExpectedLabelKeysFromSections(sections) {
|
|
62
|
+
const map = new Map();
|
|
63
|
+
for (const section of sections) {
|
|
64
|
+
const existing = map.get(section.sectionRef);
|
|
65
|
+
if (existing === undefined) {
|
|
66
|
+
map.set(section.sectionRef, section.expectedLabelKeys);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!areLabelKeyArraysEqual(existing, section.expectedLabelKeys)) {
|
|
70
|
+
throw new Error(`[collectExpectedLabelKeysFromSections] Duplicate sectionRef ` +
|
|
71
|
+
`"${section.sectionRef}" registered with ` +
|
|
72
|
+
`non-matching expectedLabelKeys. ` +
|
|
73
|
+
`First registration: [${existing.join(', ')}]. ` +
|
|
74
|
+
`Second registration: [${section.expectedLabelKeys.join(', ')}]. ` +
|
|
75
|
+
`Either rename one section's sectionRef or align both expectedLabelKeys arrays.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return map;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Order-insensitive equality for two `readonly WebsiteLabelKey[]`.
|
|
82
|
+
* Used by the collector to validate that a duplicate-ref registration
|
|
83
|
+
* is intentional (same keys) vs. a copy-paste bug (different keys).
|
|
84
|
+
*
|
|
85
|
+
* Quadratic in the array length, but `expectedLabelKeys` is bounded by
|
|
86
|
+
* the number of slots a single section consumes (typically <20), so
|
|
87
|
+
* the cost is negligible compared to surface readability of the
|
|
88
|
+
* comparison.
|
|
89
|
+
*/
|
|
90
|
+
function areLabelKeyArraysEqual(a, b) {
|
|
91
|
+
if (a.length !== b.length)
|
|
92
|
+
return false;
|
|
93
|
+
const aSet = new Set(a);
|
|
94
|
+
for (const key of b) {
|
|
95
|
+
if (!aSet.has(key))
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
/** @wildo_source:part:end saas.website.astro.label-key-collector */
|
|
101
|
+
//# sourceMappingURL=collect-expected-label-keys.js.map
|