@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,95 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { MDXProvider } from '@mdx-js/react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @wildo_source:part:start saas.website.blog.mdx-provider facet:layer:mdx facet:family:website
|
|
7
|
+
*
|
|
8
|
+
* `WebsiteMdxProvider` — thin wrapper around `@mdx-js/react`'s
|
|
9
|
+
* `MDXProvider` that scopes a consumer-supplied `components` map for
|
|
10
|
+
* the entire MDX subtree it wraps.
|
|
11
|
+
*
|
|
12
|
+
* # Why a framework wrapper at all (vs. consumers using `MDXProvider` directly)
|
|
13
|
+
*
|
|
14
|
+
* Three application-facing reasons:
|
|
15
|
+
*
|
|
16
|
+
* 1. **Single API surface for embedded business sections.** The framework
|
|
17
|
+
* owns the provider boundary so consumers
|
|
18
|
+
* never directly import from `@mdx-js/react` — they ship a single
|
|
19
|
+
* `mdxComponents` object literal and the framework picks the
|
|
20
|
+
* provider implementation. If `@mdx-js/react` ever splits its
|
|
21
|
+
* package or changes its provider semantics (it has shipped
|
|
22
|
+
* breaking changes between major versions before), the swap
|
|
23
|
+
* lands in this file with zero consumer churn.
|
|
24
|
+
* 2. **Bundle-isolation discipline.** `@mdx-js/react` is browser-safe
|
|
25
|
+
* but heavy (`mdx/types.js`, `react-context` propagation). Walling
|
|
26
|
+
* it behind a single re-export keeps the import surface auditable
|
|
27
|
+
* through the package's client-bundle boundary: the `mdx` public entry is
|
|
28
|
+
* checked separately and lists `@mdx-js/react` exactly once.
|
|
29
|
+
* 3. **`disableParentContext: false` (default) is the right default.**
|
|
30
|
+
* MDX 3 lets a nested `MDXProvider` inherit components from the
|
|
31
|
+
* parent provider, which is what we want when the per-blog-post
|
|
32
|
+
* provider sits inside a site-wide MDX provider used by landing-page
|
|
33
|
+
* islands.
|
|
34
|
+
* Pinning the option here makes the framework's posture explicit;
|
|
35
|
+
* a future change documents itself as a breaking framework decision
|
|
36
|
+
* rather than a silent `@mdx-js/react` default flip.
|
|
37
|
+
*
|
|
38
|
+
* # The `components` map shape
|
|
39
|
+
*
|
|
40
|
+
* The map is `Record<string, ComponentType<any>>` — every key is the
|
|
41
|
+
* tag name an MDX file uses (`<CTASection ref="post-cta" />` resolves
|
|
42
|
+
* via the `CTASection` key). Values are React components the consumer
|
|
43
|
+
* authored under their own `src/sections/blog.sections.tsx` and
|
|
44
|
+
* wrapped in `<WebsiteSection sectionRef={…} category={…}>` so the
|
|
45
|
+
* label-key context is established before the section's
|
|
46
|
+
* `useWebsiteLabel(slot)` calls fire. The framework does NOT inspect
|
|
47
|
+
* the shape — discipline lives in the consumer's section registry
|
|
48
|
+
* + the label-pack validation boundary (which proves the locale pack covers
|
|
49
|
+
* each section's expected keys).
|
|
50
|
+
*
|
|
51
|
+
* # Why a `ComponentType<any>` value type and not a stricter generic
|
|
52
|
+
*
|
|
53
|
+
* MDX components receive their own props (children, plus whatever the
|
|
54
|
+
* MDX author writes inline as JSX attributes). The type system cannot
|
|
55
|
+
* narrow this through the provider boundary because the MDX compiler
|
|
56
|
+
* does the routing at compile time. `ComponentType<any>` is the
|
|
57
|
+
* convention `@mdx-js/react` itself uses (`MDXComponents` in
|
|
58
|
+
* `mdx/types.js`) — staying compatible with the upstream type avoids
|
|
59
|
+
* a wrap/unwrap dance at every consumer call site.
|
|
60
|
+
*
|
|
61
|
+
* # Bundle boundary
|
|
62
|
+
*
|
|
63
|
+
* Ships into the React-island bundle (consumed transitively from
|
|
64
|
+
* `BlogPost.tsx` which itself sits behind the consumer's per-page
|
|
65
|
+
* bridge file). MUST stay clear of `node:*` and SSR-tier specifiers
|
|
66
|
+
* — the bundle-isolation test walks `mdx.ts` as one of its entries.
|
|
67
|
+
*/
|
|
68
|
+
export interface WebsiteMdxComponentsMap {
|
|
69
|
+
/**
|
|
70
|
+
* Tag-name → component map. The MDX compiler resolves each
|
|
71
|
+
* `<TagName />` in a blog post's body against this object.
|
|
72
|
+
*/
|
|
73
|
+
readonly [tagName: string]: ComponentType<any>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface WebsiteMdxProviderProps {
|
|
77
|
+
/**
|
|
78
|
+
* Consumer-supplied component overrides. Keys are MDX tag names;
|
|
79
|
+
* values are the React components those tags resolve to.
|
|
80
|
+
*/
|
|
81
|
+
components: WebsiteMdxComponentsMap;
|
|
82
|
+
/**
|
|
83
|
+
* MDX subtree (typically the compiled `<MdxBody/>` from a content
|
|
84
|
+
* collection entry). May render any number of provider-scoped
|
|
85
|
+
* components.
|
|
86
|
+
*/
|
|
87
|
+
children: ReactNode;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function WebsiteMdxProvider(props: WebsiteMdxProviderProps): ReactNode {
|
|
91
|
+
const { components, children } = props;
|
|
92
|
+
return <MDXProvider components={components}>{children}</MDXProvider>;
|
|
93
|
+
}
|
|
94
|
+
WebsiteMdxProvider.displayName = 'WebsiteMdxProvider';
|
|
95
|
+
/** @wildo_source:part:end saas.website.blog.mdx-provider */
|
package/src/mdx.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level barrel for the marketing-blog MDX adapter (Phase 7).
|
|
3
|
+
*
|
|
4
|
+
* # Why a dedicated subpath instead of folding into the root barrel
|
|
5
|
+
*
|
|
6
|
+
* `@wildo-ai/saas-website/mdx` ships into the React-island bundle
|
|
7
|
+
* (consumed transitively from a consumer's per-post `.astro` page
|
|
8
|
+
* which imports `<BlogPost>` and renders it client-side). The
|
|
9
|
+
* `@mdx-js/react` runtime adds non-trivial weight to the closure;
|
|
10
|
+
* isolating the entry behind a dedicated subpath:
|
|
11
|
+
*
|
|
12
|
+
* - keeps `index.ts` (the Node + React companion-runtime entry) free
|
|
13
|
+
* of MDX runtime dependencies for sites that ship landing-page-only
|
|
14
|
+
* marketing surfaces;
|
|
15
|
+
* - lets the bundle-isolation test walk this entry independently
|
|
16
|
+
* (see `engine/saas-website/src/__tests__/bundle-isolation.test.ts`
|
|
17
|
+
* `ENTRY_FILES` list) so an accidental `node:fs` import inside
|
|
18
|
+
* `BlogPost.tsx` regresses against the React-bundle constraint
|
|
19
|
+
* without polluting the other entries' allow-lists.
|
|
20
|
+
*
|
|
21
|
+
* Pattern mirrors `astro.ts` / `astro-island.ts` / `config-loader.ts`:
|
|
22
|
+
* one purpose, one barrel, one bundle-isolation entry.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export * from './mdx/index';
|
|
26
|
+
/**
|
|
27
|
+
* The blog adapter's per-locale bridge factory
|
|
28
|
+
* (`createWebsiteBlogPostBridgeIsland`). Lives in `astro/` beside
|
|
29
|
+
* `bridge-runtime.tsx` (same closure-over-deps pattern) but is exported
|
|
30
|
+
* from THIS entry — not `astro-island.ts` — because it hard-imports
|
|
31
|
+
* `./mdx/BlogPost`, whose `@mdx-js/react` peer is optional. Exporting it
|
|
32
|
+
* from the mandatory per-page bridge entry made the optional peer a
|
|
33
|
+
* de-facto install requirement for every non-blog site (Vite resolves the
|
|
34
|
+
* whole `export *` graph before tree-shaking). Here, only sites that opt
|
|
35
|
+
* into the blog adapter — and therefore install `@astrojs/mdx` +
|
|
36
|
+
* `@mdx-js/react` — ever resolve it.
|
|
37
|
+
*/
|
|
38
|
+
export * from './astro/blog-post-bridge';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
4
|
+
|
|
5
|
+
import { BlogPostFrontmatterSchema } from '../blog/blog-post-frontmatter.shared.schemas';
|
|
6
|
+
|
|
7
|
+
const VALID_INPUT = {
|
|
8
|
+
title: 'Why context windows matter for agentic UX',
|
|
9
|
+
summary: 'A short, focused summary of the post for OG / meta description.',
|
|
10
|
+
slug: 'why-context-matters',
|
|
11
|
+
locale: AvailableLanguage.EN,
|
|
12
|
+
publishedAt: '2026-04-17T10:30:00.000Z',
|
|
13
|
+
authorRef: 'paul-cayon',
|
|
14
|
+
tags: ['agents', 'ux'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('BlogPostFrontmatterSchema', () => {
|
|
18
|
+
it('parses a well-formed frontmatter', () => {
|
|
19
|
+
const parsed = BlogPostFrontmatterSchema.parse(VALID_INPUT);
|
|
20
|
+
expect(parsed.slug).toBe('why-context-matters');
|
|
21
|
+
expect(parsed.tags).toHaveLength(2);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('rejects malformed slug / authorRef / tags', () => {
|
|
25
|
+
expect(() =>
|
|
26
|
+
BlogPostFrontmatterSchema.parse({ ...VALID_INPUT, slug: 'Why-Context-Matters' }),
|
|
27
|
+
).toThrow();
|
|
28
|
+
expect(() =>
|
|
29
|
+
BlogPostFrontmatterSchema.parse({ ...VALID_INPUT, authorRef: 'Paul Cayon' }),
|
|
30
|
+
).toThrow();
|
|
31
|
+
expect(() =>
|
|
32
|
+
BlogPostFrontmatterSchema.parse({ ...VALID_INPUT, tags: ['UX'] }),
|
|
33
|
+
).toThrow();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rejects non-ISO publishedAt', () => {
|
|
37
|
+
expect(() =>
|
|
38
|
+
BlogPostFrontmatterSchema.parse({ ...VALID_INPUT, publishedAt: '2026-04-17' }),
|
|
39
|
+
).toThrow();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('accepts optional coverImageUrl', () => {
|
|
43
|
+
expect(() =>
|
|
44
|
+
BlogPostFrontmatterSchema.parse({
|
|
45
|
+
...VALID_INPUT,
|
|
46
|
+
coverImageUrl: 'https://cdn.example.com/blog/why-context.jpg',
|
|
47
|
+
}),
|
|
48
|
+
).not.toThrow();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Boundary test: importing the companion entry MUST NOT pull React into
|
|
5
|
+
* the evaluation graph. Implemented via a tracking Proxy installed on
|
|
6
|
+
* `globalThis` BEFORE the import: any access to a `'react'`-shaped
|
|
7
|
+
* module would evaluate React's runtime side-effects (which include
|
|
8
|
+
* registering a `__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_*`
|
|
9
|
+
* symbol on the global). We assert no such symbol exists post-import.
|
|
10
|
+
*
|
|
11
|
+
* Approach is intentionally indirect — we cannot stub `react` at the
|
|
12
|
+
* Vitest bundler level without losing the boundary check we want to
|
|
13
|
+
* make. Checking the global symbol is React's own canary for
|
|
14
|
+
* "React's runtime has been evaluated".
|
|
15
|
+
*/
|
|
16
|
+
describe('companion-exports React-free boundary', () => {
|
|
17
|
+
it('does not boot React when imported in a Node-only context', async () => {
|
|
18
|
+
const reactCanaryBefore = Object.getOwnPropertySymbols(globalThis).filter((s) =>
|
|
19
|
+
String(s).includes('react'),
|
|
20
|
+
);
|
|
21
|
+
const reactGlobalsBefore = Object.keys(globalThis).filter((k) =>
|
|
22
|
+
k.includes('__REACT_DEVTOOLS_GLOBAL_HOOK__'),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
await import('../../companion-exports');
|
|
26
|
+
|
|
27
|
+
const reactCanaryAfter = Object.getOwnPropertySymbols(globalThis).filter((s) =>
|
|
28
|
+
String(s).includes('react'),
|
|
29
|
+
);
|
|
30
|
+
const reactGlobalsAfter = Object.keys(globalThis).filter((k) =>
|
|
31
|
+
k.includes('__REACT_DEVTOOLS_GLOBAL_HOOK__'),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(reactCanaryAfter).toEqual(reactCanaryBefore);
|
|
35
|
+
expect(reactGlobalsAfter).toEqual(reactGlobalsBefore);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('exposes the expected schema surface', async () => {
|
|
39
|
+
const mod = await import('../../companion-exports');
|
|
40
|
+
expect(mod.WebsitePageRefSchema).toBeDefined();
|
|
41
|
+
expect(mod.WebsiteSectionRefSchema).toBeDefined();
|
|
42
|
+
expect(mod.WebsiteLabelKeySchema).toBeDefined();
|
|
43
|
+
expect(mod.WebsiteSectionCategorySchema).toBeDefined();
|
|
44
|
+
expect(mod.WebsiteSectionDefinitionSchema).toBeDefined();
|
|
45
|
+
expect(mod.WebsiteDesignTokensSchema).toBeDefined();
|
|
46
|
+
expect(mod.WebsitePageManifestSchema).toBeDefined();
|
|
47
|
+
expect(mod.WebsiteRootConfigSchema).toBeDefined();
|
|
48
|
+
expect(mod.BlogPostFrontmatterSchema).toBeDefined();
|
|
49
|
+
expect(mod.validateLabelPackAgainstManifest).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
WebsiteColorTokensSchema,
|
|
5
|
+
WebsiteDesignTokensSchema,
|
|
6
|
+
WebsiteRadiusTokensSchema,
|
|
7
|
+
} from '../design-tokens/website-design-tokens.shared.schemas';
|
|
8
|
+
|
|
9
|
+
describe('WebsiteColorTokensSchema', () => {
|
|
10
|
+
it('parses every required token as a non-empty string', () => {
|
|
11
|
+
const tokens = {
|
|
12
|
+
background: '#fff',
|
|
13
|
+
foreground: '#111',
|
|
14
|
+
primary: 'oklch(0.7 0.1 200)',
|
|
15
|
+
primaryForeground: '#fff',
|
|
16
|
+
accent: 'rgb(255 94 91)',
|
|
17
|
+
accentForeground: '#fff',
|
|
18
|
+
surface: 'var(--brand-surface)',
|
|
19
|
+
surfaceForeground: '#111',
|
|
20
|
+
border: '#e5e5e5',
|
|
21
|
+
muted: '#f5f5f5',
|
|
22
|
+
mutedForeground: 'currentColor',
|
|
23
|
+
};
|
|
24
|
+
expect(() => WebsiteColorTokensSchema.parse(tokens)).not.toThrow();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('rejects empty strings', () => {
|
|
28
|
+
expect(() =>
|
|
29
|
+
WebsiteColorTokensSchema.parse({
|
|
30
|
+
background: '',
|
|
31
|
+
foreground: '#111',
|
|
32
|
+
primary: '#0070f3',
|
|
33
|
+
primaryForeground: '#fff',
|
|
34
|
+
accent: '#ff5e5b',
|
|
35
|
+
accentForeground: '#fff',
|
|
36
|
+
surface: '#fafafa',
|
|
37
|
+
surfaceForeground: '#111',
|
|
38
|
+
border: '#e5e5e5',
|
|
39
|
+
muted: '#f5f5f5',
|
|
40
|
+
mutedForeground: '#737373',
|
|
41
|
+
}),
|
|
42
|
+
).toThrow();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('WebsiteRadiusTokensSchema', () => {
|
|
47
|
+
it('parses every required token as a non-empty string', () => {
|
|
48
|
+
expect(() =>
|
|
49
|
+
WebsiteRadiusTokensSchema.parse({
|
|
50
|
+
none: '0',
|
|
51
|
+
sm: '0.125rem',
|
|
52
|
+
md: '0.375rem',
|
|
53
|
+
lg: '0.75rem',
|
|
54
|
+
xl: '1.5rem',
|
|
55
|
+
full: '9999px',
|
|
56
|
+
}),
|
|
57
|
+
).not.toThrow();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('CSS-injection guard on token values (Phase 6 fix H-α)', () => {
|
|
62
|
+
const validRadii = {
|
|
63
|
+
none: '0',
|
|
64
|
+
sm: '0.125rem',
|
|
65
|
+
md: '0.375rem',
|
|
66
|
+
lg: '0.75rem',
|
|
67
|
+
xl: '1.5rem',
|
|
68
|
+
full: '9999px',
|
|
69
|
+
};
|
|
70
|
+
const validColors = {
|
|
71
|
+
background: '#fff',
|
|
72
|
+
foreground: '#111',
|
|
73
|
+
primary: '#0070f3',
|
|
74
|
+
primaryForeground: '#fff',
|
|
75
|
+
accent: '#ff5e5b',
|
|
76
|
+
accentForeground: '#fff',
|
|
77
|
+
surface: '#fafafa',
|
|
78
|
+
surfaceForeground: '#111',
|
|
79
|
+
border: '#e5e5e5',
|
|
80
|
+
muted: '#f5f5f5',
|
|
81
|
+
mutedForeground: '#737373',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
it.each([
|
|
85
|
+
['semicolon', 'red; color: blue'],
|
|
86
|
+
['close-brace', 'red } body { display: none'],
|
|
87
|
+
['open-brace', 'red { hack: 1'],
|
|
88
|
+
['block-comment-open', 'red /* hack'],
|
|
89
|
+
['block-comment-close', 'red */ hack'],
|
|
90
|
+
['newline', 'red\nfake: 1'],
|
|
91
|
+
['carriage-return', 'red\rfake: 1'],
|
|
92
|
+
])('rejects color values containing %s', (_name, value) => {
|
|
93
|
+
expect(() =>
|
|
94
|
+
WebsiteColorTokensSchema.parse({ ...validColors, primary: value }),
|
|
95
|
+
).toThrow();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it.each([
|
|
99
|
+
['style-element terminator', '#fff</style><svg onload=alert(1)>'],
|
|
100
|
+
['raw open-angle-bracket', '#fff<script'],
|
|
101
|
+
['raw close-angle-bracket', '#fff>hack'],
|
|
102
|
+
])(
|
|
103
|
+
'rejects color values containing HTML break-out metacharacters (%s) — Phase 6 fourth audit fix H-Χ',
|
|
104
|
+
(_name, value) => {
|
|
105
|
+
expect(() =>
|
|
106
|
+
WebsiteColorTokensSchema.parse({ ...validColors, primary: value }),
|
|
107
|
+
).toThrow();
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
it.each([
|
|
112
|
+
['semicolon', '0.5rem; color: red'],
|
|
113
|
+
['close-brace', '0.5rem }'],
|
|
114
|
+
['block-comment-open', '0.5rem /*'],
|
|
115
|
+
['style-element terminator (H-Χ)', '0.5rem</style>'],
|
|
116
|
+
['raw open-angle-bracket (H-Χ)', '0.5rem<'],
|
|
117
|
+
['raw close-angle-bracket (H-Χ)', '0.5rem>'],
|
|
118
|
+
])('rejects radius values containing %s', (_name, value) => {
|
|
119
|
+
expect(() => WebsiteRadiusTokensSchema.parse({ ...validRadii, md: value })).toThrow();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('accepts legitimate var() references with parentheses (no metacharacters)', () => {
|
|
123
|
+
expect(() =>
|
|
124
|
+
WebsiteColorTokensSchema.parse({ ...validColors, primary: 'var(--brand-primary)' }),
|
|
125
|
+
).not.toThrow();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('accepts oklch() functional notation', () => {
|
|
129
|
+
expect(() =>
|
|
130
|
+
WebsiteColorTokensSchema.parse({ ...validColors, primary: 'oklch(0.7 0.1 200 / 0.5)' }),
|
|
131
|
+
).not.toThrow();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('WebsiteDesignTokensSchema', () => {
|
|
136
|
+
it('parses the colors+radii bundle', () => {
|
|
137
|
+
const parsed = WebsiteDesignTokensSchema.parse({
|
|
138
|
+
colors: {
|
|
139
|
+
background: '#fff',
|
|
140
|
+
foreground: '#111',
|
|
141
|
+
primary: '#0070f3',
|
|
142
|
+
primaryForeground: '#fff',
|
|
143
|
+
accent: '#ff5e5b',
|
|
144
|
+
accentForeground: '#fff',
|
|
145
|
+
surface: '#fafafa',
|
|
146
|
+
surfaceForeground: '#111',
|
|
147
|
+
border: '#e5e5e5',
|
|
148
|
+
muted: '#f5f5f5',
|
|
149
|
+
mutedForeground: '#737373',
|
|
150
|
+
},
|
|
151
|
+
radii: {
|
|
152
|
+
none: '0',
|
|
153
|
+
sm: '0.125rem',
|
|
154
|
+
md: '0.375rem',
|
|
155
|
+
lg: '0.75rem',
|
|
156
|
+
xl: '1.5rem',
|
|
157
|
+
full: '9999px',
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
expect(parsed.colors.primary).toBe('#0070f3');
|
|
161
|
+
expect(parsed.radii.full).toBe('9999px');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
WebsiteLabelKeySchema,
|
|
5
|
+
composeWebsiteLabelKey,
|
|
6
|
+
} from '../label-keys/website-label-key.schemas';
|
|
7
|
+
import { WebsitePageRefSchema } from '../refs/page-ref.schemas';
|
|
8
|
+
import { WebsiteSectionRefSchema } from '../refs/section-ref.schemas';
|
|
9
|
+
|
|
10
|
+
describe('WebsiteLabelKeySchema', () => {
|
|
11
|
+
it('accepts simple page.section.slot keys', () => {
|
|
12
|
+
expect(WebsiteLabelKeySchema.parse('website.landing.hero.headline')).toBe(
|
|
13
|
+
'website.landing.hero.headline',
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('accepts dot-nested and bracket-indexed slots', () => {
|
|
18
|
+
expect(WebsiteLabelKeySchema.parse('website.landing.hero.cta.primary')).toBeDefined();
|
|
19
|
+
expect(WebsiteLabelKeySchema.parse('website.pricing.tiers.tiers[0].name')).toBeDefined();
|
|
20
|
+
expect(
|
|
21
|
+
WebsiteLabelKeySchema.parse('website.contact.form.field.email-placeholder'),
|
|
22
|
+
).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('rejects non-website prefixes and missing parts', () => {
|
|
26
|
+
expect(() => WebsiteLabelKeySchema.parse('app.landing.hero.headline')).toThrow();
|
|
27
|
+
expect(() => WebsiteLabelKeySchema.parse('website.landing.hero')).toThrow();
|
|
28
|
+
expect(() => WebsiteLabelKeySchema.parse('website..hero.headline')).toThrow();
|
|
29
|
+
expect(() => WebsiteLabelKeySchema.parse('website.Landing.hero.headline')).toThrow();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('composeWebsiteLabelKey', () => {
|
|
34
|
+
it('round-trips through the full ref → key pipeline', () => {
|
|
35
|
+
const pageRef = WebsitePageRefSchema.parse('landing');
|
|
36
|
+
const sectionRef = WebsiteSectionRefSchema.parse('hero');
|
|
37
|
+
const key = composeWebsiteLabelKey({ pageRef, sectionRef, slotPath: 'headline' });
|
|
38
|
+
expect(key).toBe('website.landing.hero.headline');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('throws on stale refs that would compose an invalid key', () => {
|
|
42
|
+
expect(() =>
|
|
43
|
+
composeWebsiteLabelKey({ pageRef: 'Landing', sectionRef: 'hero', slotPath: 'headline' }),
|
|
44
|
+
).toThrow();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { WebsiteLabelKeySchema, type WebsiteLabelKey } from '../label-keys/website-label-key.schemas';
|
|
4
|
+
import { WebsitePageManifestSchema, type WebsitePageManifest } from '../manifests/website-page-manifest.shared.schemas';
|
|
5
|
+
import { WebsitePageRefSchema } from '../refs/page-ref.schemas';
|
|
6
|
+
import { WebsiteSectionRefSchema } from '../refs/section-ref.schemas';
|
|
7
|
+
import {
|
|
8
|
+
validateLabelPackAgainstManifest,
|
|
9
|
+
type ExpectedLabelKeysBySectionRef,
|
|
10
|
+
type LabelPack,
|
|
11
|
+
} from '../validators/label-pack.validator';
|
|
12
|
+
|
|
13
|
+
const NoopComponent = () => null;
|
|
14
|
+
|
|
15
|
+
const HERO_REF = WebsiteSectionRefSchema.parse('hero');
|
|
16
|
+
const CTA_REF = WebsiteSectionRefSchema.parse('cta');
|
|
17
|
+
|
|
18
|
+
const HERO_KEYS: readonly WebsiteLabelKey[] = [
|
|
19
|
+
WebsiteLabelKeySchema.parse('website.landing.hero.headline'),
|
|
20
|
+
WebsiteLabelKeySchema.parse('website.landing.hero.subheadline'),
|
|
21
|
+
];
|
|
22
|
+
const CTA_KEYS: readonly WebsiteLabelKey[] = [
|
|
23
|
+
WebsiteLabelKeySchema.parse('website.landing.cta.headline'),
|
|
24
|
+
WebsiteLabelKeySchema.parse('website.landing.cta.primary-cta'),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// The SCHEMA infers `pageComponent` as `unknown` (it is validated as an opaque value), while the
|
|
28
|
+
// declared `WebsitePageManifest` the validator takes says `ComponentType`. The parse still runs —
|
|
29
|
+
// this fixture is checked against the schema — and the annotation names the one field on which
|
|
30
|
+
// the two disagree.
|
|
31
|
+
const MANIFEST: WebsitePageManifest = WebsitePageManifestSchema.parse({
|
|
32
|
+
ref: WebsitePageRefSchema.parse('landing'),
|
|
33
|
+
routePath: '/',
|
|
34
|
+
pageComponent: NoopComponent,
|
|
35
|
+
sectionRefs: [HERO_REF, CTA_REF],
|
|
36
|
+
metaLabelKeys: {
|
|
37
|
+
titleKey: WebsiteLabelKeySchema.parse('website.landing.meta.title'),
|
|
38
|
+
descriptionKey: WebsiteLabelKeySchema.parse('website.landing.meta.description'),
|
|
39
|
+
},
|
|
40
|
+
}) as WebsitePageManifest;
|
|
41
|
+
|
|
42
|
+
const EXPECTED: ExpectedLabelKeysBySectionRef = new Map([
|
|
43
|
+
[HERO_REF, HERO_KEYS],
|
|
44
|
+
[CTA_REF, CTA_KEYS],
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
describe('validateLabelPackAgainstManifest', () => {
|
|
48
|
+
it('returns empty diagnostics for a complete pack', () => {
|
|
49
|
+
const pack: LabelPack = {
|
|
50
|
+
'website.landing.meta.title': 'Welcome',
|
|
51
|
+
'website.landing.meta.description': 'A great product',
|
|
52
|
+
'website.landing.hero.headline': 'Hello',
|
|
53
|
+
'website.landing.hero.subheadline': 'World',
|
|
54
|
+
'website.landing.cta.headline': 'Try now',
|
|
55
|
+
'website.landing.cta.primary-cta': 'Sign up',
|
|
56
|
+
};
|
|
57
|
+
const result = validateLabelPackAgainstManifest([MANIFEST], EXPECTED, pack);
|
|
58
|
+
expect(result.missing).toEqual([]);
|
|
59
|
+
expect(result.orphaned).toEqual([]);
|
|
60
|
+
expect(result.unmappedSectionRefs).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('reports missing keys (sorted)', () => {
|
|
64
|
+
const pack: LabelPack = {
|
|
65
|
+
'website.landing.meta.title': 'Welcome',
|
|
66
|
+
'website.landing.meta.description': 'A great product',
|
|
67
|
+
'website.landing.hero.headline': 'Hello',
|
|
68
|
+
};
|
|
69
|
+
const result = validateLabelPackAgainstManifest([MANIFEST], EXPECTED, pack);
|
|
70
|
+
expect(result.missing).toEqual([
|
|
71
|
+
'website.landing.cta.headline',
|
|
72
|
+
'website.landing.cta.primary-cta',
|
|
73
|
+
'website.landing.hero.subheadline',
|
|
74
|
+
]);
|
|
75
|
+
expect(result.orphaned).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('reports orphaned keys (sorted)', () => {
|
|
79
|
+
const pack: LabelPack = {
|
|
80
|
+
'website.landing.meta.title': 'Welcome',
|
|
81
|
+
'website.landing.meta.description': 'A great product',
|
|
82
|
+
'website.landing.hero.headline': 'Hello',
|
|
83
|
+
'website.landing.hero.subheadline': 'World',
|
|
84
|
+
'website.landing.cta.headline': 'Try now',
|
|
85
|
+
'website.landing.cta.primary-cta': 'Sign up',
|
|
86
|
+
'website.landing.hero.stale-key-1': 'orphan',
|
|
87
|
+
'website.landing.cta.stale-key-2': 'orphan',
|
|
88
|
+
};
|
|
89
|
+
const result = validateLabelPackAgainstManifest([MANIFEST], EXPECTED, pack);
|
|
90
|
+
expect(result.missing).toEqual([]);
|
|
91
|
+
expect(result.orphaned).toEqual([
|
|
92
|
+
'website.landing.cta.stale-key-2',
|
|
93
|
+
'website.landing.hero.stale-key-1',
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('surfaces unmapped section refs separately from missing keys', () => {
|
|
98
|
+
const expectedPartial: ExpectedLabelKeysBySectionRef = new Map([[HERO_REF, HERO_KEYS]]);
|
|
99
|
+
const pack: LabelPack = {
|
|
100
|
+
'website.landing.meta.title': 'Welcome',
|
|
101
|
+
'website.landing.meta.description': 'A great product',
|
|
102
|
+
'website.landing.hero.headline': 'Hello',
|
|
103
|
+
'website.landing.hero.subheadline': 'World',
|
|
104
|
+
};
|
|
105
|
+
const result = validateLabelPackAgainstManifest([MANIFEST], expectedPartial, pack);
|
|
106
|
+
expect(result.unmappedSectionRefs).toEqual([CTA_REF]);
|
|
107
|
+
expect(result.missing).toEqual([]);
|
|
108
|
+
expect(result.orphaned).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('treats chromeLabelKeys as expected — declared chrome keys are NOT orphaned', () => {
|
|
112
|
+
const chromeKeys: readonly WebsiteLabelKey[] = [
|
|
113
|
+
WebsiteLabelKeySchema.parse('website.chrome.header.brand'),
|
|
114
|
+
WebsiteLabelKeySchema.parse('website.chrome.footer.copyright'),
|
|
115
|
+
];
|
|
116
|
+
const pack: LabelPack = {
|
|
117
|
+
'website.landing.meta.title': 'Welcome',
|
|
118
|
+
'website.landing.meta.description': 'A great product',
|
|
119
|
+
'website.landing.hero.headline': 'Hello',
|
|
120
|
+
'website.landing.hero.subheadline': 'World',
|
|
121
|
+
'website.landing.cta.headline': 'Try now',
|
|
122
|
+
'website.landing.cta.primary-cta': 'Sign up',
|
|
123
|
+
'website.chrome.header.brand': 'Acme',
|
|
124
|
+
'website.chrome.footer.copyright': '© 2026',
|
|
125
|
+
};
|
|
126
|
+
const result = validateLabelPackAgainstManifest([MANIFEST], EXPECTED, pack, chromeKeys);
|
|
127
|
+
expect(result.missing).toEqual([]);
|
|
128
|
+
expect(result.orphaned).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('reports missing chromeLabelKeys alongside missing section keys', () => {
|
|
132
|
+
const chromeKeys: readonly WebsiteLabelKey[] = [
|
|
133
|
+
WebsiteLabelKeySchema.parse('website.chrome.header.brand'),
|
|
134
|
+
];
|
|
135
|
+
const pack: LabelPack = {
|
|
136
|
+
'website.landing.meta.title': 'Welcome',
|
|
137
|
+
'website.landing.meta.description': 'A great product',
|
|
138
|
+
'website.landing.hero.headline': 'Hello',
|
|
139
|
+
'website.landing.hero.subheadline': 'World',
|
|
140
|
+
'website.landing.cta.headline': 'Try now',
|
|
141
|
+
'website.landing.cta.primary-cta': 'Sign up',
|
|
142
|
+
};
|
|
143
|
+
const result = validateLabelPackAgainstManifest([MANIFEST], EXPECTED, pack, chromeKeys);
|
|
144
|
+
expect(result.missing).toEqual(['website.chrome.header.brand']);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { WebsiteLabelKeySchema } from '../label-keys/website-label-key.schemas';
|
|
4
|
+
import { WebsitePageManifestSchema } from '../manifests/website-page-manifest.shared.schemas';
|
|
5
|
+
import { WebsitePageRefSchema } from '../refs/page-ref.schemas';
|
|
6
|
+
import { WebsiteSectionRefSchema } from '../refs/section-ref.schemas';
|
|
7
|
+
|
|
8
|
+
const NoopPageComponent = () => null;
|
|
9
|
+
|
|
10
|
+
const VALID_MANIFEST_INPUT = {
|
|
11
|
+
ref: WebsitePageRefSchema.parse('landing'),
|
|
12
|
+
routePath: '/' as unknown,
|
|
13
|
+
pageComponent: NoopPageComponent,
|
|
14
|
+
sectionRefs: [WebsiteSectionRefSchema.parse('hero'), WebsiteSectionRefSchema.parse('cta')],
|
|
15
|
+
metaLabelKeys: {
|
|
16
|
+
titleKey: WebsiteLabelKeySchema.parse('website.landing.meta.title'),
|
|
17
|
+
descriptionKey: WebsiteLabelKeySchema.parse('website.landing.meta.description'),
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('WebsitePageManifestSchema', () => {
|
|
22
|
+
it('parses a well-formed manifest', () => {
|
|
23
|
+
const parsed = WebsitePageManifestSchema.parse(VALID_MANIFEST_INPUT);
|
|
24
|
+
expect(parsed.ref).toBe('landing');
|
|
25
|
+
expect(parsed.sectionRefs).toHaveLength(2);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('parses Astro-style parameter routes', () => {
|
|
29
|
+
expect(() =>
|
|
30
|
+
WebsitePageManifestSchema.parse({
|
|
31
|
+
...VALID_MANIFEST_INPUT,
|
|
32
|
+
routePath: '/blog/[slug]',
|
|
33
|
+
}),
|
|
34
|
+
).not.toThrow();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('rejects duplicate section refs', () => {
|
|
38
|
+
expect(() =>
|
|
39
|
+
WebsitePageManifestSchema.parse({
|
|
40
|
+
...VALID_MANIFEST_INPUT,
|
|
41
|
+
sectionRefs: [WebsiteSectionRefSchema.parse('hero'), WebsiteSectionRefSchema.parse('hero')],
|
|
42
|
+
}),
|
|
43
|
+
).toThrow(/Duplicate sectionRef/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('rejects meta label keys not rooted under the page ref', () => {
|
|
47
|
+
expect(() =>
|
|
48
|
+
WebsitePageManifestSchema.parse({
|
|
49
|
+
...VALID_MANIFEST_INPUT,
|
|
50
|
+
metaLabelKeys: {
|
|
51
|
+
titleKey: WebsiteLabelKeySchema.parse('website.pricing.meta.title'),
|
|
52
|
+
descriptionKey: WebsiteLabelKeySchema.parse('website.landing.meta.description'),
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
).toThrow(/must be rooted under the page ref/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('rejects empty section ref lists', () => {
|
|
59
|
+
expect(() =>
|
|
60
|
+
WebsitePageManifestSchema.parse({
|
|
61
|
+
...VALID_MANIFEST_INPUT,
|
|
62
|
+
sectionRefs: [],
|
|
63
|
+
}),
|
|
64
|
+
).toThrow();
|
|
65
|
+
});
|
|
66
|
+
});
|